Skip to content

Commit

Permalink
Fix WebSocket support with Jetty 10.0.x
Browse files Browse the repository at this point in the history
Fixes gh-26847
  • Loading branch information
wilkinsona committed Jun 16, 2021
1 parent d635e1e commit bc7004d
Show file tree
Hide file tree
Showing 31 changed files with 2,135 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2012-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.boot.autoconfigure.websocket.servlet;

import java.lang.reflect.Method;

import javax.servlet.ServletContext;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.AbstractConfiguration;
import org.eclipse.jetty.webapp.WebAppContext;

import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.core.Ordered;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

/**
* WebSocket customizer for {@link JettyServletWebServerFactory} with Jetty 10.
*
* @author Andy Wilkinson
*/
class Jetty10WebSocketServletWebServerCustomizer
implements WebServerFactoryCustomizer<JettyServletWebServerFactory>, Ordered {

static final String JETTY_WEB_SOCKET_SERVER_CONTAINER = "org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer";

static final String JAVAX_WEB_SOCKET_SERVER_CONTAINER = "org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer";

@Override
public void customize(JettyServletWebServerFactory factory) {
factory.addConfigurations(new AbstractConfiguration() {

@Override
public void configure(WebAppContext context) throws Exception {
ServletContext servletContext = context.getServletContext();
Class<?> jettyContainer = ClassUtils.forName(JETTY_WEB_SOCKET_SERVER_CONTAINER, null);
Method getJettyContainer = ReflectionUtils.findMethod(jettyContainer, "getContainer",
ServletContext.class);
Server server = context.getServer();
if (ReflectionUtils.invokeMethod(getJettyContainer, null, servletContext) == null) {
ensureWebSocketComponents(server, servletContext);
ensureContainer(jettyContainer, servletContext);
}
Class<?> javaxContainer = ClassUtils.forName(JAVAX_WEB_SOCKET_SERVER_CONTAINER, null);
Method getJavaxContainer = ReflectionUtils.findMethod(javaxContainer, "getContainer",
ServletContext.class);
if (ReflectionUtils.invokeMethod(getJavaxContainer, "getContainer", servletContext) == null) {
ensureWebSocketComponents(server, servletContext);
ensureUpgradeFilter(servletContext);
ensureMappings(servletContext);
ensureContainer(javaxContainer, servletContext);
}
}

private void ensureWebSocketComponents(Server server, ServletContext servletContext)
throws ClassNotFoundException {
Class<?> webSocketServerComponents = ClassUtils
.forName("org.eclipse.jetty.websocket.core.server.WebSocketServerComponents", null);
Method ensureWebSocketComponents = ReflectionUtils.findMethod(webSocketServerComponents,
"ensureWebSocketComponents", Server.class, ServletContext.class);
ReflectionUtils.invokeMethod(ensureWebSocketComponents, null, server, servletContext);
}

private void ensureContainer(Class<?> container, ServletContext servletContext)
throws ClassNotFoundException {
Method ensureContainer = ReflectionUtils.findMethod(container, "ensureContainer", ServletContext.class);
ReflectionUtils.invokeMethod(ensureContainer, null, servletContext);
}

private void ensureUpgradeFilter(ServletContext servletContext) throws ClassNotFoundException {
Class<?> webSocketUpgradeFilter = ClassUtils
.forName("org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter", null);
Method ensureFilter = ReflectionUtils.findMethod(webSocketUpgradeFilter, "ensureFilter",
ServletContext.class);
ReflectionUtils.invokeMethod(ensureFilter, null, servletContext);
}

private void ensureMappings(ServletContext servletContext) throws ClassNotFoundException {
Class<?> webSocketMappings = ClassUtils
.forName("org.eclipse.jetty.websocket.core.server.WebSocketMappings", null);
Method ensureMappings = ReflectionUtils.findMethod(webSocketMappings, "ensureMappings",
ServletContext.class);
ReflectionUtils.invokeMethod(ensureMappings, null, servletContext);
}

});
}

@Override
public int getOrder() {
return 0;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-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.
Expand Down Expand Up @@ -83,6 +83,19 @@ JettyWebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() {

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = { Jetty10WebSocketServletWebServerCustomizer.JAVAX_WEB_SOCKET_SERVER_CONTAINER,
Jetty10WebSocketServletWebServerCustomizer.JETTY_WEB_SOCKET_SERVER_CONTAINER })
static class Jetty10WebSocketConfiguration {

@Bean
@ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
Jetty10WebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() {
return new Jetty10WebSocketServletWebServerCustomizer();
}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(io.undertow.websockets.jsr.Bootstrap.class)
static class UndertowWebSocketConfiguration {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
id "java"
id "org.springframework.boot.conventions"
}

description = "Spring Boot Jetty 10 smoke test"

dependencies {
implementation(enforcedPlatform("org.eclipse.jetty:jetty-bom:10.0.5"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jetty")) {
exclude group: "org.eclipse.jetty.websocket", module: "websocket-server"
exclude group: "org.eclipse.jetty.websocket", module: "javax-websocket-server-impl"
}
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) {
exclude module: "spring-boot-starter-tomcat"
}

testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2012-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 smoketest.jetty10;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.springframework.stereotype.Component;

/**
* Simple {@link ServletContextListener} to test gh-2058.
*/
@Component
public class ExampleServletContextListener implements ServletContextListener {

@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("*** contextInitialized");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("*** contextDestroyed");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2012-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 smoketest.jetty10;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SampleJetty10Application {

public static void main(String[] args) {
SpringApplication.run(SampleJetty10Application.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2012-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 smoketest.jetty10.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class HelloWorldService {

@Value("${name:World}")
private String name;

public String getHelloMessage() {
return "Hello " + this.name;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2012-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 smoketest.jetty10.web;

import smoketest.jetty10.service.HelloWorldService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class SampleController {

@Autowired
private HelloWorldService helloWorldService;

@GetMapping("/")
@ResponseBody
public String helloWorld() {
return this.helloWorldService.getHelloMessage();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
server.compression.enabled: true
server.compression.min-response-size: 1
server.jetty.threads.acceptors=2
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2012-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 smoketest.jetty10;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPInputStream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StreamUtils;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Basic integration tests for demo application.
*
* @author Dave Syer
* @author Andy Wilkinson
*/
@EnabledForJreRange(min = JRE.JAVA_11)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class SampleJetty10ApplicationTests {

@Autowired
private TestRestTemplate restTemplate;

@Test
void testHome() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("Hello World");
}

@Test
void testCompression() throws Exception {
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("Accept-Encoding", "gzip");
HttpEntity<?> requestEntity = new HttpEntity<>(requestHeaders);
ResponseEntity<byte[]> entity = this.restTemplate.exchange("/", HttpMethod.GET, requestEntity, byte[].class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
try (GZIPInputStream inflater = new GZIPInputStream(new ByteArrayInputStream(entity.getBody()))) {
assertThat(StreamUtils.copyToString(inflater, StandardCharsets.UTF_8)).isEqualTo("Hello World");
}
}

}

0 comments on commit bc7004d

Please sign in to comment.