Commit df5f5912 authored by Andy Wilkinson's avatar Andy Wilkinson

Support Jetty 10

Closes gh-24886
parent a95e93a8
......@@ -59,6 +59,9 @@ Spring Boot supports the following embedded servlet containers:
| Jetty 9.4
| 3.1
| Jetty 10.0
| 4.0
| Undertow 2.0
| 4.0
|===
......
......@@ -451,7 +451,8 @@ The following Maven example shows how to exclude Tomcat and include Jetty for Sp
</dependency>
----
NOTE: The version of the Servlet API has been overridden as, unlike Tomcat 9 and Undertow 2.0, Jetty 9.4 does not support Servlet 4.0.
NOTE: The version of the Servlet API has been overridden as, unlike Tomcat 9 and Undertow 2, Jetty 9.4 does not support Servlet 4.0.
If you wish to use Jetty 10, which does support Servlet 4.0, override the `jetty.version` property rather than the `servlet-api.version` property.
The following Gradle example shows how to use Undertow in place of Reactor Netty for Spring WebFlux:
......
/*
* Copyright 2012-2020 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.
......@@ -16,7 +16,10 @@
package org.springframework.boot.web.embedded.jetty;
import java.lang.reflect.Method;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
......@@ -27,6 +30,7 @@ import org.eclipse.jetty.server.Server;
import org.springframework.boot.web.server.GracefulShutdownCallback;
import org.springframework.boot.web.server.GracefulShutdownResult;
import org.springframework.core.log.LogMessage;
import org.springframework.util.ReflectionUtils;
/**
* Handles Jetty graceful shutdown.
......@@ -50,23 +54,44 @@ final class GracefulShutdown {
void shutDownGracefully(GracefulShutdownCallback callback) {
logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
boolean jetty10 = isJetty10();
for (Connector connector : this.server.getConnectors()) {
shutdown(connector);
shutdown(connector, !jetty10);
}
this.shuttingDown = true;
new Thread(() -> awaitShutdown(callback), "jetty-shutdown").start();
}
private void shutdown(Connector connector) {
@SuppressWarnings("unchecked")
private void shutdown(Connector connector, boolean getResult) {
Future<Void> result;
try {
connector.shutdown().get();
result = connector.shutdown();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
catch (NoSuchMethodError ex) {
Method shutdown = ReflectionUtils.findMethod(connector.getClass(), "shutdown");
result = (Future<Void>) ReflectionUtils.invokeMethod(shutdown, connector);
}
if (getResult) {
try {
result.get();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
catch (ExecutionException ex) {
// Continue
}
}
}
private boolean isJetty10() {
try {
return CompletableFuture.class.equals(Connector.class.getMethod("shutdown").getReturnType());
}
catch (ExecutionException ex) {
// Continue
catch (Exception ex) {
return false;
}
}
......
/*
* Copyright 2012-2020 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.
......@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
......@@ -49,11 +50,11 @@ class JettyEmbeddedErrorHandler extends ErrorPageErrorHandler {
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException {
throws IOException, ServletException {
if (!HANDLED_HTTP_METHODS.contains(baseRequest.getMethod())) {
baseRequest.setMethod("GET");
}
super.doError(target, baseRequest, request, response);
super.handle(target, baseRequest, request, response);
}
}
/*
* 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.
......@@ -47,7 +47,12 @@ final class JettyHandlerWrappers {
handler.addIncludedMethods(httpMethod.name());
}
if (compression.getExcludedUserAgents() != null) {
handler.setExcludedAgentPatterns(compression.getExcludedUserAgents());
try {
handler.setExcludedAgentPatterns(compression.getExcludedUserAgents());
}
catch (NoSuchMethodError ex) {
// Jetty 10 does not support User-Agent-based exclusions
}
}
return handler;
}
......
/*
* Copyright 2012-2020 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.
......@@ -19,6 +19,7 @@ package org.springframework.boot.web.embedded.jetty;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
......@@ -70,6 +71,7 @@ import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
......@@ -306,7 +308,16 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor
holder.setInitParameter("dirAllowed", "false");
holder.setInitOrder(1);
context.getServletHandler().addServletWithMapping(holder, "/");
context.getServletHandler().getServletMapping("/").setDefault(true);
ServletMapping servletMapping = context.getServletHandler().getServletMapping("/");
try {
servletMapping.setDefault(true);
}
catch (NoSuchMethodError ex) {
// Jetty 10
Method setFromDefaultDescriptor = ReflectionUtils.findMethod(servletMapping.getClass(),
"setFromDefaultDescriptor", boolean.class);
ReflectionUtils.invokeMethod(setFromDefaultDescriptor, servletMapping, true);
}
}
/**
......
......@@ -32,7 +32,6 @@ import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.springframework.boot.web.server.GracefulShutdownCallback;
import org.springframework.boot.web.server.GracefulShutdownResult;
......@@ -120,18 +119,7 @@ public class JettyWebServer implements WebServer {
// Cache the connectors and then remove them to prevent requests being
// handled before the application context is ready.
this.connectors = this.server.getConnectors();
this.server.addBean(new AbstractLifeCycle() {
@Override
protected void doStart() throws Exception {
for (Connector connector : JettyWebServer.this.connectors) {
Assert.state(connector.isStopped(),
() -> "Connector " + connector + " has been started prematurely");
}
JettyWebServer.this.server.setConnectors(null);
}
});
JettyWebServer.this.server.setConnectors(null);
// Start the server so that the ServletContext is available
this.server.start();
this.server.setStopAtShutdown(false);
......
/*
* Copyright 2012-2020 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.
......@@ -106,8 +106,22 @@ class SslServerCustomizer implements JettyServerCustomizer {
private ServerConnector createHttp11ServerConnector(Server server, HttpConfiguration config,
SslContextFactory.Server sslContextFactory) {
HttpConnectionFactory connectionFactory = new HttpConnectionFactory(config);
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory,
HttpVersion.HTTP_1_1.asString());
SslConnectionFactory sslConnectionFactory;
try {
sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
}
catch (NoSuchMethodError ex) {
// Jetty 10
try {
sslConnectionFactory = SslConnectionFactory.class
.getConstructor(SslContextFactory.Server.class, String.class)
.newInstance(sslContextFactory, HttpVersion.HTTP_1_1.asString());
}
catch (Exception ex2) {
throw new RuntimeException(ex2);
}
}
return new SslValidatingServerConnector(server, sslContextFactory, this.ssl.getKeyAlias(), sslConnectionFactory,
connectionFactory);
}
......
/*
* 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.web.embedded.jetty;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.boot.testsupport.classpath.ClassPathOverrides;
import static org.assertj.core.api.Assertions.assertThat;
@EnabledForJreRange(min = JRE.JAVA_11)
@ClassPathExclusions({ "jetty-*.jar", "tomcat-embed*.jar" })
@ClassPathOverrides({ "org.slf4j:slf4j-api:1.7.25", "org.eclipse.jetty:jetty-io:10.0.0",
"org.eclipse.jetty:jetty-server:10.0.0", "org.eclipse.jetty:jetty-servlet:10.0.0",
"org.eclipse.jetty:jetty-util:10.0.0", "org.eclipse.jetty:jetty-webapp:10.0.0",
"org.eclipse.jetty.http2:http2-common:10.0.0", "org.eclipse.jetty.http2:http2-hpack:10.0.0",
"org.eclipse.jetty.http2:http2-server:10.0.0", "org.mortbay.jasper:apache-jsp:8.5.40" })
public class Jetty10ServletWebServerFactoryTests extends JettyServletWebServerFactoryTests {
@Override
@Test
protected void correctVersionOfJettyUsed() {
String jettyVersion = ErrorHandler.class.getPackage().getImplementationVersion();
assertThat(jettyVersion.startsWith("10.0"));
}
@Override
@Disabled("Jetty 10 does not support User-Agent-based compression")
protected void noCompressionForUserAgent() {
}
@Override
@Disabled("Jetty 10 adds methods to Configuration that we can't mock while compiling against 9")
protected void jettyConfigurations() throws Exception {
}
}
/*
* Copyright 2012-2020 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.
......@@ -16,11 +16,13 @@
package org.springframework.boot.web.embedded.jetty;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EventListener;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
......@@ -46,6 +48,7 @@ import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.eclipse.jetty.webapp.AbstractConfiguration;
......@@ -59,6 +62,7 @@ import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
......@@ -77,7 +81,7 @@ import static org.mockito.Mockito.mock;
class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFactoryTests {
@Test
void correctVersionOfJettyUsed() {
protected void correctVersionOfJettyUsed() {
String jettyVersion = ErrorHandler.class.getPackage().getImplementationVersion();
Matcher matcher = Pattern.compile("[0-9]+.[0-9]+.([0-9]+)[\\.-].*").matcher(jettyVersion);
assertThat(matcher.find()).isTrue();
......@@ -85,7 +89,7 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac
}
@Test
void jettyConfigurations() throws Exception {
protected void jettyConfigurations() throws Exception {
JettyServletWebServerFactory factory = getFactory();
Configuration[] configurations = new Configuration[4];
Arrays.setAll(configurations, (i) -> mock(Configuration.class));
......@@ -143,9 +147,9 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac
JettyWebServer jettyWebServer = (JettyWebServer) this.webServer;
ServerConnector connector = (ServerConnector) jettyWebServer.getServer().getConnectors()[0];
SslConnectionFactory connectionFactory = connector.getConnectionFactory(SslConnectionFactory.class);
assertThat(connectionFactory.getSslContextFactory().getIncludeCipherSuites()).containsExactly("ALPHA", "BRAVO",
"CHARLIE");
assertThat(connectionFactory.getSslContextFactory().getExcludeCipherSuites()).isEmpty();
SslContextFactory sslContextFactory = extractSslContextFactory(connectionFactory);
assertThat(sslContextFactory.getIncludeCipherSuites()).containsExactly("ALPHA", "BRAVO", "CHARLIE");
assertThat(sslContextFactory.getExcludeCipherSuites()).isEmpty();
}
@Test
......@@ -166,8 +170,8 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac
JettyWebServer jettyWebServer = (JettyWebServer) this.webServer;
ServerConnector connector = (ServerConnector) jettyWebServer.getServer().getConnectors()[0];
SslConnectionFactory connectionFactory = connector.getConnectionFactory(SslConnectionFactory.class);
assertThat(connectionFactory.getSslContextFactory().getIncludeProtocols()).containsExactly("TLSv1.1",
"TLSv1.2");
SslContextFactory sslContextFactory = extractSslContextFactory(connectionFactory);
assertThat(sslContextFactory.getIncludeProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
}
@Test
......@@ -179,7 +183,19 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac
JettyWebServer jettyWebServer = (JettyWebServer) this.webServer;
ServerConnector connector = (ServerConnector) jettyWebServer.getServer().getConnectors()[0];
SslConnectionFactory connectionFactory = connector.getConnectionFactory(SslConnectionFactory.class);
assertThat(connectionFactory.getSslContextFactory().getIncludeProtocols()).containsExactly("TLSv1.1");
SslContextFactory sslContextFactory = extractSslContextFactory(connectionFactory);
assertThat(sslContextFactory.getIncludeProtocols()).containsExactly("TLSv1.1");
}
private SslContextFactory extractSslContextFactory(SslConnectionFactory connectionFactory) {
try {
return connectionFactory.getSslContextFactory();
}
catch (NoSuchMethodError ex) {
Method getSslContextFactory = ReflectionUtils.findMethod(connectionFactory.getClass(),
"getSslContextFactory");
return (SslContextFactory) ReflectionUtils.invokeMethod(getSslContextFactory, connectionFactory);
}
}
@Test
......@@ -372,7 +388,7 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac
JettyServletWebServerFactory factory = getFactory();
factory.addServerCustomizers((JettyServerCustomizer) (server) -> {
Collection<WebAppContext> contexts = server.getBeans(WebAppContext.class);
contexts.iterator().next().addEventListener(new ServletContextListener() {
EventListener eventListener = new ServletContextListener() {
@Override
public void contextInitialized(ServletContextEvent event) {
......@@ -382,8 +398,17 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac
@Override
public void contextDestroyed(ServletContextEvent event) {
}
});
};
WebAppContext context = contexts.iterator().next();
try {
context.addEventListener(eventListener);
}
catch (NoSuchMethodError ex) {
// Jetty 10
Method addEventListener = ReflectionUtils.findMethod(context.getClass(), "addEventListener",
EventListener.class);
ReflectionUtils.invokeMethod(addEventListener, context, eventListener);
}
});
assertThatExceptionOfType(WebServerException.class).isThrownBy(() -> {
JettyWebServer jettyWebServer = (JettyWebServer) factory.getWebServer();
......
......@@ -842,7 +842,7 @@ public abstract class AbstractServletWebServerFactoryTests {
}
@Test
void noCompressionForUserAgent() throws Exception {
protected void noCompressionForUserAgent() throws Exception {
assertThat(doTestCompression(10000, null, new String[] { "testUserAgent" })).isFalse();
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment