Skip to content

Commit 39c3827

Browse files
committed
Initialize WebSocket infrastructure when using WebFlux and Jetty
In Spring Framework 5.x with Jetty 9, the reactive JettyRequestUpgradeStrategy was able to initialize Jetty's WebSocket infrastructure itself. With Jetty 10 this is no longer possible and Boot must perform the initialization as part of preparing the reactive JettyWebServer. This commit updates the reactive WebSocket auto-configuration to initialize Jetty's WebSocket infrastructure as part of creating the reactive JettyWebServer. Fixes gh-33347
1 parent 641f00f commit 39c3827

File tree

3 files changed

+239
-1
lines changed

3 files changed

+239
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2012-2023 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.boot.autoconfigure.websocket.reactive;
18+
19+
import jakarta.servlet.ServletContext;
20+
import org.eclipse.jetty.server.Handler;
21+
import org.eclipse.jetty.server.handler.HandlerCollection;
22+
import org.eclipse.jetty.server.handler.HandlerWrapper;
23+
import org.eclipse.jetty.servlet.ServletContextHandler;
24+
import org.eclipse.jetty.websocket.core.server.WebSocketMappings;
25+
import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
26+
import org.eclipse.jetty.websocket.jakarta.server.internal.JakartaWebSocketServerContainer;
27+
import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer;
28+
import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter;
29+
30+
import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory;
31+
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
32+
import org.springframework.core.Ordered;
33+
34+
/**
35+
* WebSocket customizer for {@link JettyReactiveWebServerFactory}.
36+
*
37+
* @author Andy Wilkinson
38+
* @since 3.0.8
39+
*/
40+
public class JettyWebSocketReactiveWebServerCustomizer
41+
implements WebServerFactoryCustomizer<JettyReactiveWebServerFactory>, Ordered {
42+
43+
@Override
44+
public void customize(JettyReactiveWebServerFactory factory) {
45+
factory.addServerCustomizers((server) -> {
46+
ServletContextHandler servletContextHandler = findServletContextHandler(server);
47+
if (servletContextHandler != null) {
48+
ServletContext servletContext = servletContextHandler.getServletContext();
49+
if (JettyWebSocketServerContainer.getContainer(servletContext) == null) {
50+
WebSocketServerComponents.ensureWebSocketComponents(server, servletContext);
51+
JettyWebSocketServerContainer.ensureContainer(servletContext);
52+
}
53+
if (JakartaWebSocketServerContainer.getContainer(servletContext) == null) {
54+
WebSocketServerComponents.ensureWebSocketComponents(server, servletContext);
55+
WebSocketUpgradeFilter.ensureFilter(servletContext);
56+
WebSocketMappings.ensureMappings(servletContext);
57+
JakartaWebSocketServerContainer.ensureContainer(servletContext);
58+
}
59+
}
60+
});
61+
}
62+
63+
private ServletContextHandler findServletContextHandler(Handler handler) {
64+
if (handler instanceof ServletContextHandler servletContextHandler) {
65+
return servletContextHandler;
66+
}
67+
if (handler instanceof HandlerWrapper handlerWrapper) {
68+
return findServletContextHandler(handlerWrapper.getHandler());
69+
}
70+
if (handler instanceof HandlerCollection handlerCollection) {
71+
for (Handler contained : handlerCollection.getHandlers()) {
72+
ServletContextHandler servletContextHandler = findServletContextHandler(contained);
73+
if (servletContextHandler != null) {
74+
return servletContextHandler;
75+
}
76+
}
77+
}
78+
return null;
79+
}
80+
81+
@Override
82+
public int getOrder() {
83+
return 0;
84+
}
85+
86+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/WebSocketReactiveAutoConfiguration.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2023 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.
@@ -20,6 +20,7 @@
2020
import jakarta.websocket.server.ServerContainer;
2121
import org.apache.catalina.startup.Tomcat;
2222
import org.apache.tomcat.websocket.server.WsSci;
23+
import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer;
2324

2425
import org.springframework.boot.autoconfigure.AutoConfiguration;
2526
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -57,4 +58,16 @@ TomcatWebSocketReactiveWebServerCustomizer websocketReactiveWebServerCustomizer(
5758

5859
}
5960

61+
@Configuration(proxyBeanMethods = false)
62+
@ConditionalOnClass(JakartaWebSocketServletContainerInitializer.class)
63+
static class JettyWebSocketConfiguration {
64+
65+
@Bean
66+
@ConditionalOnMissingBean(name = "websocketReactiveWebServerCustomizer")
67+
JettyWebSocketReactiveWebServerCustomizer websocketServletWebServerCustomizer() {
68+
return new JettyWebSocketReactiveWebServerCustomizer();
69+
}
70+
71+
}
72+
6073
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright 2012-2023 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.boot.autoconfigure.websocket.reactive;
18+
19+
import java.util.function.Function;
20+
import java.util.stream.Stream;
21+
22+
import jakarta.servlet.ServletContext;
23+
import jakarta.websocket.server.ServerContainer;
24+
import org.apache.catalina.Container;
25+
import org.apache.catalina.Context;
26+
import org.apache.catalina.startup.Tomcat;
27+
import org.eclipse.jetty.servlet.ServletContextHandler;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.Arguments;
30+
import org.junit.jupiter.params.provider.MethodSource;
31+
32+
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
33+
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
34+
import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides;
35+
import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory;
36+
import org.springframework.boot.web.embedded.jetty.JettyWebServer;
37+
import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory;
38+
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
39+
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
40+
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
41+
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
42+
import org.springframework.context.annotation.Bean;
43+
import org.springframework.context.annotation.Configuration;
44+
import org.springframework.http.server.reactive.HttpHandler;
45+
46+
import static org.assertj.core.api.Assertions.assertThat;
47+
48+
/**
49+
* Tests for {@link WebSocketReactiveAutoConfiguration}.
50+
*
51+
* @author Andy Wilkinson
52+
*/
53+
@DirtiesUrlFactories
54+
class WebSocketReactiveAutoConfigurationTests {
55+
56+
@ParameterizedTest(name = "{0}")
57+
@MethodSource("testConfiguration")
58+
@ForkedClassPath
59+
void serverContainerIsAvailableFromTheServletContext(String server,
60+
Function<AnnotationConfigReactiveWebServerApplicationContext, ServletContext> servletContextAccessor,
61+
Class<?>... configuration) {
62+
try (AnnotationConfigReactiveWebServerApplicationContext context = new AnnotationConfigReactiveWebServerApplicationContext(
63+
configuration)) {
64+
Object serverContainer = servletContextAccessor.apply(context)
65+
.getAttribute("jakarta.websocket.server.ServerContainer");
66+
assertThat(serverContainer).isInstanceOf(ServerContainer.class);
67+
}
68+
}
69+
70+
static Stream<Arguments> testConfiguration() {
71+
return Stream.of(Arguments.of("Jetty",
72+
(Function<AnnotationConfigReactiveWebServerApplicationContext, ServletContext>) WebSocketReactiveAutoConfigurationTests::getJettyServletContext,
73+
new Class<?>[] { JettyConfiguration.class,
74+
WebSocketReactiveAutoConfiguration.JettyWebSocketConfiguration.class }),
75+
Arguments.of("Tomcat",
76+
(Function<AnnotationConfigReactiveWebServerApplicationContext, ServletContext>) WebSocketReactiveAutoConfigurationTests::getTomcatServletContext,
77+
new Class<?>[] { TomcatConfiguration.class,
78+
WebSocketReactiveAutoConfiguration.TomcatWebSocketConfiguration.class }));
79+
}
80+
81+
private static ServletContext getJettyServletContext(AnnotationConfigReactiveWebServerApplicationContext context) {
82+
return ((ServletContextHandler) ((JettyWebServer) context.getWebServer()).getServer().getHandler())
83+
.getServletContext();
84+
}
85+
86+
private static ServletContext getTomcatServletContext(AnnotationConfigReactiveWebServerApplicationContext context) {
87+
return findContext(((TomcatWebServer) context.getWebServer()).getTomcat()).getServletContext();
88+
}
89+
90+
private static Context findContext(Tomcat tomcat) {
91+
for (Container child : tomcat.getHost().findChildren()) {
92+
if (child instanceof Context context) {
93+
return context;
94+
}
95+
}
96+
throw new IllegalStateException("The host does not contain a Context");
97+
}
98+
99+
@Configuration(proxyBeanMethods = false)
100+
static class CommonConfiguration {
101+
102+
@Bean
103+
static WebServerFactoryCustomizerBeanPostProcessor webServerFactoryCustomizerBeanPostProcessor() {
104+
return new WebServerFactoryCustomizerBeanPostProcessor();
105+
}
106+
107+
@Bean
108+
HttpHandler echoHandler() {
109+
return (request, response) -> response.writeWith(request.getBody());
110+
}
111+
112+
}
113+
114+
@Configuration(proxyBeanMethods = false)
115+
static class TomcatConfiguration extends CommonConfiguration {
116+
117+
@Bean
118+
ReactiveWebServerFactory webServerFactory() {
119+
TomcatReactiveWebServerFactory factory = new TomcatReactiveWebServerFactory();
120+
factory.setPort(0);
121+
return factory;
122+
}
123+
124+
}
125+
126+
@Servlet5ClassPathOverrides
127+
@Configuration(proxyBeanMethods = false)
128+
static class JettyConfiguration extends CommonConfiguration {
129+
130+
@Bean
131+
ReactiveWebServerFactory webServerFactory() {
132+
JettyReactiveWebServerFactory factory = new JettyReactiveWebServerFactory();
133+
factory.setPort(0);
134+
return factory;
135+
}
136+
137+
}
138+
139+
}

0 commit comments

Comments
 (0)