Support Jetty 10
Closes gh-24886
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
catch (ExecutionException ex) {
|
||||
// Continue
|
||||
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 (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user