Create spring-boot-jetty module

This commit is contained in:
Andy Wilkinson
2025-03-13 17:01:55 +00:00
committed by Phillip Webb
parent 0a72db4676
commit c96b7375fe
68 changed files with 220 additions and 172 deletions

View File

@@ -1,72 +0,0 @@
/*
* Copyright 2012-2025 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.server.jetty;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
/**
* {@link ConfigurableWebServerFactory} for Jetty-specific features.
*
* @author Brian Clozel
* @author Moritz Halbritter
* @since 4.0.0
*/
public interface ConfigurableJettyWebServerFactory extends ConfigurableWebServerFactory {
/**
* Set the number of acceptor threads to use.
* @param acceptors the number of acceptor threads to use
*/
void setAcceptors(int acceptors);
/**
* Set the {@link ThreadPool} that should be used by the {@link Server}. If set to
* {@code null} (default), the {@link Server} creates a {@link ThreadPool} implicitly.
* @param threadPool the ThreadPool to be used
*/
void setThreadPool(ThreadPool threadPool);
/**
* Set the number of selector threads to use.
* @param selectors the number of selector threads to use
*/
void setSelectors(int selectors);
/**
* Set if x-forward-* headers should be processed.
* @param useForwardHeaders if x-forward headers should be used
*/
void setUseForwardHeaders(boolean useForwardHeaders);
/**
* Add {@link JettyServerCustomizer}s that will be applied to the {@link Server}
* before it is started.
* @param customizers the customizers to add
*/
void addServerCustomizers(JettyServerCustomizer... customizers);
/**
* Sets the maximum number of concurrent connections.
* @param maxConnections the maximum number of concurrent connections
* @since 4.0.0
*/
void setMaxConnections(int maxConnections);
}

View File

@@ -1,45 +0,0 @@
/*
* Copyright 2012-2025 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.server.jetty;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Server;
/**
* {@link JettyServerCustomizer} to add {@link ForwardedRequestCustomizer}.
*
* @author Phillip Webb
* @since 4.0.0
*/
public class ForwardHeadersCustomizer implements JettyServerCustomizer {
@Override
public void customize(Server server) {
ForwardedRequestCustomizer customizer = new ForwardedRequestCustomizer();
for (Connector connector : server.getConnectors()) {
for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) {
if (connectionFactory instanceof HttpConfiguration.ConnectionFactory jettyConnectionFactory) {
jettyConnectionFactory.getHttpConfiguration().addCustomizer(customizer);
}
}
}
}
}

View File

@@ -1,124 +0,0 @@
/*
* Copyright 2012-2024 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.server.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;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.springframework.boot.web.server.GracefulShutdownCallback;
import org.springframework.boot.web.server.GracefulShutdownResult;
import org.springframework.util.ReflectionUtils;
/**
* Handles Jetty graceful shutdown.
*
* @author Andy Wilkinson
* @author Onur Kagan Ozcan
*/
final class GracefulShutdown {
private static final Log logger = LogFactory.getLog(GracefulShutdown.class);
private final Server server;
private final Supplier<Integer> activeRequests;
private volatile boolean aborted = false;
GracefulShutdown(Server server, Supplier<Integer> activeRequests) {
this.server = server;
this.activeRequests = activeRequests;
}
void shutDownGracefully(GracefulShutdownCallback callback) {
logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
new Thread(() -> awaitShutdown(callback), "jetty-shutdown").start();
boolean jetty10 = isJetty10();
for (Connector connector : this.server.getConnectors()) {
shutdown(connector, !jetty10);
}
}
@SuppressWarnings("unchecked")
private void shutdown(Connector connector, boolean getResult) {
Future<Void> result;
try {
result = connector.shutdown();
}
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 (Exception ex) {
return false;
}
}
private void awaitShutdown(GracefulShutdownCallback callback) {
while (!this.aborted && this.activeRequests.get() > 0) {
sleep(100);
}
if (this.aborted) {
logger.info("Graceful shutdown aborted with one or more requests still active");
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
}
else {
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);
}
}
private void sleep(long millis) {
try {
Thread.sleep(millis);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
void abort() {
this.aborted = true;
}
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2012-2025 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.server.jetty;
import org.eclipse.jetty.http.HttpFields.Mutable;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.util.Callback;
import org.springframework.boot.web.server.Compression;
/**
* Jetty {@code HandlerWrapper} static factory.
*
* @author Brian Clozel
*/
final class JettyHandlerWrappers {
private JettyHandlerWrappers() {
}
static Handler.Wrapper createGzipHandlerWrapper(Compression compression) {
GzipHandler handler = new GzipHandler();
handler.setMinGzipSize((int) compression.getMinResponseSize().toBytes());
handler.setIncludedMimeTypes(compression.getMimeTypes());
for (HttpMethod httpMethod : HttpMethod.values()) {
handler.addIncludedMethods(httpMethod.name());
}
return handler;
}
static Handler.Wrapper createServerHeaderHandlerWrapper(String header) {
return new ServerHeaderHandler(header);
}
/**
* {@link Handler.Wrapper} to add a custom {@code server} header.
*/
private static class ServerHeaderHandler extends Handler.Wrapper {
private static final String SERVER_HEADER = "server";
private final String value;
ServerHeaderHandler(String value) {
this.value = value;
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception {
Mutable headers = response.getHeaders();
if (!headers.contains(SERVER_HEADER)) {
headers.add(SERVER_HEADER, this.value);
}
return super.handle(request, response, callback);
}
}
}

View File

@@ -1,39 +0,0 @@
/*
* Copyright 2012-2025 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.server.jetty;
import org.eclipse.jetty.server.Server;
import org.springframework.boot.web.server.servlet.jetty.JettyServletWebServerFactory;
/**
* Callback interface that can be used to customize a Jetty {@link Server}.
*
* @author Dave Syer
* @since 4.0.0
* @see JettyServletWebServerFactory
*/
@FunctionalInterface
public interface JettyServerCustomizer {
/**
* Customize the server.
* @param server the server to customize
*/
void customize(Server server);
}

View File

@@ -1,335 +0,0 @@
/*
* Copyright 2012-2025 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.server.jetty;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.springframework.boot.web.server.GracefulShutdownCallback;
import org.springframework.boot.web.server.GracefulShutdownResult;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.boot.web.server.reactive.jetty.JettyReactiveWebServerFactory;
import org.springframework.http.server.reactive.ServletHttpHandlerAdapter;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link WebServer} that can be used to control a Jetty web server.
*
* @author Phillip Webb
* @author Dave Syer
* @author David Liu
* @author Eddú Meléndez
* @author Brian Clozel
* @author Kristine Jetzke
* @since 4.0.0
* @see JettyReactiveWebServerFactory
*/
public class JettyWebServer implements WebServer {
private static final Log logger = LogFactory.getLog(JettyWebServer.class);
private final Object monitor = new Object();
private final Server server;
private final boolean autoStart;
private final GracefulShutdown gracefulShutdown;
private Connector[] connectors;
private volatile boolean started;
/**
* Create a new {@link JettyWebServer} instance.
* @param server the underlying Jetty server
*/
public JettyWebServer(Server server) {
this(server, true);
}
/**
* Create a new {@link JettyWebServer} instance.
* @param server the underlying Jetty server
* @param autoStart if auto-starting the server
*/
public JettyWebServer(Server server, boolean autoStart) {
this.autoStart = autoStart;
Assert.notNull(server, "'server' must not be null");
this.server = server;
this.gracefulShutdown = createGracefulShutdown(server);
initialize();
}
private GracefulShutdown createGracefulShutdown(Server server) {
StatisticsHandler statisticsHandler = findStatisticsHandler(server);
if (statisticsHandler == null) {
return null;
}
return new GracefulShutdown(server, statisticsHandler::getRequestsActive);
}
private StatisticsHandler findStatisticsHandler(Server server) {
return findStatisticsHandler(server.getHandler());
}
private StatisticsHandler findStatisticsHandler(Handler handler) {
if (handler instanceof StatisticsHandler statisticsHandler) {
return statisticsHandler;
}
if (handler instanceof Handler.Wrapper handlerWrapper) {
return findStatisticsHandler(handlerWrapper.getHandler());
}
return null;
}
private void initialize() {
synchronized (this.monitor) {
try {
// Cache the connectors and then remove them to prevent requests being
// handled before the application context is ready.
this.connectors = this.server.getConnectors();
JettyWebServer.this.server.setConnectors(null);
// Start the server so that the ServletContext is available
this.server.start();
this.server.setStopAtShutdown(false);
}
catch (Throwable ex) {
// Ensure process isn't left running
stopSilently();
throw new WebServerException("Unable to start embedded Jetty web server", ex);
}
}
}
private void stopSilently() {
try {
this.server.stop();
}
catch (Exception ex) {
// Ignore
}
}
@Override
public void start() throws WebServerException {
synchronized (this.monitor) {
if (this.started) {
return;
}
this.server.setConnectors(this.connectors);
if (!this.autoStart) {
return;
}
try {
this.server.start();
handleDeferredInitialize(this.server);
Connector[] connectors = this.server.getConnectors();
for (Connector connector : connectors) {
try {
connector.start();
}
catch (IOException ex) {
if (connector instanceof NetworkConnector networkConnector) {
PortInUseException.throwIfPortBindingException(ex, networkConnector::getPort);
}
throw ex;
}
}
this.started = true;
logger.info(getStartedLogMessage());
}
catch (WebServerException ex) {
stopSilently();
throw ex;
}
catch (Exception ex) {
stopSilently();
throw new WebServerException("Unable to start embedded Jetty server", ex);
}
}
}
String getStartedLogMessage() {
String contextPath = getContextPath();
return "Jetty started on " + getActualPortsDescription()
+ ((contextPath != null) ? " with context path '" + contextPath + "'" : "");
}
private String getActualPortsDescription() {
StringBuilder description = new StringBuilder("port");
Connector[] connectors = this.server.getConnectors();
if (connectors.length != 1) {
description.append("s");
}
description.append(" ");
for (int i = 0; i < connectors.length; i++) {
if (i != 0) {
description.append(", ");
}
Connector connector = connectors[i];
description.append(getLocalPort(connector)).append(getProtocols(connector));
}
return description.toString();
}
private String getProtocols(Connector connector) {
List<String> protocols = connector.getProtocols();
return " (" + StringUtils.collectionToDelimitedString(protocols, ", ") + ")";
}
private String getContextPath() {
List<ContextHandler> imperativeContextHandlers = this.server.getHandlers()
.stream()
.map(this::findContextHandler)
.filter(Objects::nonNull)
.filter(this::isImperative)
.toList();
if (imperativeContextHandlers.isEmpty()) {
return null;
}
return imperativeContextHandlers.stream().map(ContextHandler::getContextPath).collect(Collectors.joining(" "));
}
private ContextHandler findContextHandler(Handler handler) {
while (handler instanceof Handler.Wrapper handlerWrapper) {
if (handler instanceof ContextHandler contextHandler) {
return contextHandler;
}
handler = handlerWrapper.getHandler();
}
return null;
}
private boolean isImperative(ContextHandler contextHandler) {
if (contextHandler instanceof ServletContextHandler servletContextHandler) {
Collection<ServletHolder> servletHolders = servletContextHandler.getServletHandler()
.getBeans(ServletHolder.class);
for (ServletHolder servletHolder : servletHolders) {
if (ServletHttpHandlerAdapter.class.getName().equals(servletHolder.getClassName())) {
return false;
}
}
}
return true;
}
/**
* Performs any necessary handling of deferred initialization.
* @param server the server that has been started
* @throws Exception if a failure occurs during the deferred initialization
*/
protected void handleDeferredInitialize(Server server) throws Exception {
}
@Override
public void stop() {
synchronized (this.monitor) {
this.started = false;
if (this.gracefulShutdown != null) {
this.gracefulShutdown.abort();
}
try {
for (Connector connector : this.server.getConnectors()) {
connector.stop();
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
catch (Exception ex) {
throw new WebServerException("Unable to stop embedded Jetty server", ex);
}
}
}
@Override
public void destroy() {
synchronized (this.monitor) {
try {
this.server.stop();
}
catch (Exception ex) {
throw new WebServerException("Unable to destroy embedded Jetty server", ex);
}
}
}
@Override
public int getPort() {
Connector[] connectors = this.server.getConnectors();
for (Connector connector : connectors) {
int localPort = getLocalPort(connector);
if (localPort > 0) {
return localPort;
}
}
return -1;
}
private int getLocalPort(Connector connector) {
if (connector instanceof NetworkConnector networkConnector) {
return networkConnector.getLocalPort();
}
return 0;
}
/**
* Initiates a graceful shutdown of the Jetty web server. Handling of new requests is
* prevented and the given {@code callback} is invoked at the end of the attempt. The
* attempt can be explicitly ended by invoking {@link #stop}.
* <p>
* Once shutdown has been initiated Jetty will reject any new connections. Requests on
* existing connections will be accepted, however, a {@code Connection: close} header
* will be returned in the response.
*/
@Override
public void shutDownGracefully(GracefulShutdownCallback callback) {
if (this.gracefulShutdown == null) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
return;
}
this.gracefulShutdown.shutDownGracefully(callback);
}
/**
* Returns access to the underlying Jetty Server.
* @return the Jetty server
*/
public Server getServer() {
return this.server;
}
}

View File

@@ -1,222 +0,0 @@
/*
* Copyright 2012-2025 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.server.jetty;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import org.eclipse.jetty.ee10.webapp.Configuration;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.server.AbstractConnector;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Base class for factories that produce a {@link JettyWebServer}.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
public class JettyWebServerFactory extends AbstractConfigurableWebServerFactory
implements ConfigurableJettyWebServerFactory {
private int acceptors = -1;
private ThreadPool threadPool;
private int selectors = -1;
private List<Configuration> configurations = new ArrayList<>();
private boolean useForwardHeaders;
private Set<JettyServerCustomizer> jettyServerCustomizers = new LinkedHashSet<>();
private int maxConnections = -1;
public JettyWebServerFactory() {
}
public JettyWebServerFactory(int port) {
super(port);
}
public int getAcceptors() {
return this.acceptors;
}
@Override
public void setAcceptors(int acceptors) {
this.acceptors = acceptors;
}
public int getSelectors() {
return this.selectors;
}
@Override
public void setSelectors(int selectors) {
this.selectors = selectors;
}
public int getMaxConnections() {
return this.maxConnections;
}
@Override
public void setMaxConnections(int maxConnections) {
this.maxConnections = maxConnections;
}
/**
* Returns a mutable collection of Jetty {@link JettyServerCustomizer}s that will be
* applied to the {@link Server} before it is created.
* @return the {@link JettyServerCustomizer}s
*/
public Collection<JettyServerCustomizer> getServerCustomizers() {
return this.jettyServerCustomizers;
}
/**
* Sets {@link JettyServerCustomizer}s that will be applied to the {@link Server}
* before it is started. Calling this method will replace any existing customizers.
* @param customizers the Jetty customizers to apply
*/
public void setServerCustomizers(Collection<? extends JettyServerCustomizer> customizers) {
Assert.notNull(customizers, "'customizers' must not be null");
this.jettyServerCustomizers = new LinkedHashSet<>(customizers);
}
@Override
public void addServerCustomizers(JettyServerCustomizer... customizers) {
Assert.notNull(customizers, "'customizers' must not be null");
this.jettyServerCustomizers.addAll(Arrays.asList(customizers));
}
/**
* Returns a mutable collection of Jetty {@link Configuration}s that will be applied
* to the {@link WebAppContext} before the server is created.
* @return the Jetty {@link Configuration}s
*/
public Collection<Configuration> getConfigurations() {
return this.configurations;
}
/**
* Sets Jetty {@link Configuration}s that will be applied to the {@link WebAppContext}
* before the server is created. Calling this method will replace any existing
* configurations.
* @param configurations the Jetty configurations to apply
*/
public void setConfigurations(Collection<? extends Configuration> configurations) {
Assert.notNull(configurations, "'configurations' must not be null");
this.configurations = new ArrayList<>(configurations);
}
/**
* Add {@link Configuration}s that will be applied to the {@link WebAppContext} before
* the server is started.
* @param configurations the configurations to add
*/
public void addConfigurations(Configuration... configurations) {
Assert.notNull(configurations, "'configurations' must not be null");
this.configurations.addAll(Arrays.asList(configurations));
}
/**
* Returns a Jetty {@link ThreadPool} that should be used by the {@link Server}.
* @return a Jetty {@link ThreadPool} or {@code null}
*/
public ThreadPool getThreadPool() {
return this.threadPool;
}
@Override
public void setThreadPool(ThreadPool threadPool) {
this.threadPool = threadPool;
}
public boolean isUseForwardHeaders() {
return this.useForwardHeaders;
}
@Override
public void setUseForwardHeaders(boolean useForwardHeaders) {
this.useForwardHeaders = useForwardHeaders;
}
protected AbstractConnector createConnector(InetSocketAddress address, Server server) {
return this.createConnector(address, server, null, null, null);
}
protected AbstractConnector createConnector(InetSocketAddress address, Server server, Executor executor,
Scheduler scheduler, ByteBufferPool pool) {
HttpConfiguration httpConfiguration = new HttpConfiguration();
httpConfiguration.setSendServerVersion(false);
List<ConnectionFactory> connectionFactories = new ArrayList<>();
connectionFactories.add(new HttpConnectionFactory(httpConfiguration));
if (getHttp2() != null && getHttp2().isEnabled()) {
connectionFactories.add(new HTTP2CServerConnectionFactory(httpConfiguration));
}
ServerConnector connector = new ServerConnector(server, executor, scheduler, pool, this.getAcceptors(),
this.getSelectors(), connectionFactories.toArray(new ConnectionFactory[0]));
connector.setHost(address.getHostString());
connector.setPort(address.getPort());
return connector;
}
protected void customizeSsl(Server server, InetSocketAddress address) {
Assert.state(getSsl().getServerNameBundles().isEmpty(), "Server name SSL bundles are not supported with Jetty");
new SslServerCustomizer(getHttp2(), address, getSsl().getClientAuth(), getSslBundle()).customize(server);
}
protected Handler addHandlerWrappers(Handler handler) {
if (getCompression() != null && getCompression().getEnabled()) {
handler = applyWrapper(handler, JettyHandlerWrappers.createGzipHandlerWrapper(getCompression()));
}
if (StringUtils.hasText(getServerHeader())) {
handler = applyWrapper(handler, JettyHandlerWrappers.createServerHeaderHandlerWrapper(getServerHeader()));
}
return handler;
}
protected Handler applyWrapper(Handler handler, Handler.Wrapper wrapper) {
wrapper.setHandler(handler);
return wrapper;
}
}

View File

@@ -1,225 +0,0 @@
/*
* Copyright 2012-2025 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.server.jetty;
import java.net.InetSocketAddress;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http2.HTTP2Cipher;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleKey;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.boot.web.server.Http2;
import org.springframework.boot.web.server.Ssl.ClientAuth;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* {@link JettyServerCustomizer} that configures SSL on the given Jetty server instance.
*
* @author Brian Clozel
* @author Olivier Lamy
* @author Chris Bono
* @author Cyril Dangerville
* @author Scott Frederick
*/
class SslServerCustomizer implements JettyServerCustomizer {
private final Http2 http2;
private final InetSocketAddress address;
private final ClientAuth clientAuth;
private final SslBundle sslBundle;
SslServerCustomizer(Http2 http2, InetSocketAddress address, ClientAuth clientAuth, SslBundle sslBundle) {
this.address = address;
this.clientAuth = clientAuth;
this.sslBundle = sslBundle;
this.http2 = http2;
}
@Override
public void customize(Server server) {
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setEndpointIdentificationAlgorithm(null);
configureSsl(sslContextFactory, this.clientAuth);
ServerConnector connector = createConnector(server, sslContextFactory);
server.setConnectors(new Connector[] { connector });
}
private ServerConnector createConnector(Server server, SslContextFactory.Server sslContextFactory) {
HttpConfiguration config = new HttpConfiguration();
config.setSendServerVersion(false);
config.setSecureScheme("https");
config.setSecurePort(this.address.getPort());
config.addCustomizer(new SecureRequestCustomizer());
ServerConnector connector = createServerConnector(server, sslContextFactory, config);
connector.setPort(this.address.getPort());
connector.setHost(this.address.getHostString());
return connector;
}
private ServerConnector createServerConnector(Server server, SslContextFactory.Server sslContextFactory,
HttpConfiguration config) {
if (this.http2 == null || !this.http2.isEnabled()) {
return createHttp11ServerConnector(config, sslContextFactory, server);
}
Assert.state(isJettyAlpnPresent(),
() -> "An 'org.eclipse.jetty:jetty-alpn-*-server' dependency is required for HTTP/2 support.");
Assert.state(isJettyHttp2Present(),
() -> "The 'org.eclipse.jetty.http2:jetty-http2-server' dependency is required for HTTP/2 support.");
return createHttp2ServerConnector(config, sslContextFactory, server);
}
private ServerConnector createHttp11ServerConnector(HttpConfiguration config,
SslContextFactory.Server sslContextFactory, Server server) {
SslConnectionFactory sslConnectionFactory = createSslConnectionFactory(sslContextFactory,
HttpVersion.HTTP_1_1.asString());
HttpConnectionFactory connectionFactory = new HttpConnectionFactory(config);
return new SslValidatingServerConnector(this.sslBundle.getKey(), sslContextFactory, server,
sslConnectionFactory, connectionFactory);
}
private SslConnectionFactory createSslConnectionFactory(SslContextFactory.Server sslContextFactory,
String protocol) {
return new SslConnectionFactory(sslContextFactory, protocol);
}
private boolean isJettyAlpnPresent() {
return ClassUtils.isPresent("org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory", null);
}
private boolean isJettyHttp2Present() {
return ClassUtils.isPresent("org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory", null);
}
private ServerConnector createHttp2ServerConnector(HttpConfiguration config,
SslContextFactory.Server sslContextFactory, Server server) {
HttpConnectionFactory http = new HttpConnectionFactory(config);
HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(config);
ALPNServerConnectionFactory alpn = createAlpnServerConnectionFactory();
sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR);
if (isConscryptPresent()) {
sslContextFactory.setProvider("Conscrypt");
}
SslConnectionFactory sslConnectionFactory = createSslConnectionFactory(sslContextFactory, alpn.getProtocol());
return new SslValidatingServerConnector(this.sslBundle.getKey(), sslContextFactory, server,
sslConnectionFactory, alpn, h2, http);
}
private ALPNServerConnectionFactory createAlpnServerConnectionFactory() {
try {
return new ALPNServerConnectionFactory();
}
catch (IllegalStateException ex) {
throw new IllegalStateException(
"An 'org.eclipse.jetty:jetty-alpn-*-server' dependency is required for HTTP/2 support.", ex);
}
}
private boolean isConscryptPresent() {
return ClassUtils.isPresent("org.conscrypt.Conscrypt", null)
&& ClassUtils.isPresent("org.eclipse.jetty.alpn.conscrypt.server.ConscryptServerALPNProcessor", null);
}
/**
* Configure the SSL connection.
* @param factory the Jetty {@link Server SslContextFactory.Server}.
* @param clientAuth the client authentication mode
*/
protected void configureSsl(SslContextFactory.Server factory, ClientAuth clientAuth) {
SslBundleKey key = this.sslBundle.getKey();
SslOptions options = this.sslBundle.getOptions();
SslStoreBundle stores = this.sslBundle.getStores();
factory.setProtocol(this.sslBundle.getProtocol());
configureSslClientAuth(factory, clientAuth);
if (stores.getKeyStorePassword() != null) {
factory.setKeyStorePassword(stores.getKeyStorePassword());
}
factory.setCertAlias(key.getAlias());
if (options.getCiphers() != null) {
factory.setIncludeCipherSuites(options.getCiphers());
factory.setExcludeCipherSuites();
}
if (options.getEnabledProtocols() != null) {
factory.setIncludeProtocols(options.getEnabledProtocols());
factory.setExcludeProtocols();
}
try {
if (key.getPassword() != null) {
factory.setKeyManagerPassword(key.getPassword());
}
factory.setKeyStore(stores.getKeyStore());
factory.setTrustStore(stores.getTrustStore());
}
catch (Exception ex) {
throw new IllegalStateException("Unable to set SSL store: " + ex.getMessage(), ex);
}
}
private void configureSslClientAuth(SslContextFactory.Server factory, ClientAuth clientAuth) {
factory.setWantClientAuth(clientAuth == ClientAuth.WANT || clientAuth == ClientAuth.NEED);
factory.setNeedClientAuth(clientAuth == ClientAuth.NEED);
}
/**
* A {@link ServerConnector} that validates the ssl key alias on server startup.
*/
static class SslValidatingServerConnector extends ServerConnector {
private final SslBundleKey key;
private final SslContextFactory sslContextFactory;
SslValidatingServerConnector(SslBundleKey key, SslContextFactory sslContextFactory, Server server,
SslConnectionFactory sslConnectionFactory, HttpConnectionFactory connectionFactory) {
super(server, sslConnectionFactory, connectionFactory);
this.key = key;
this.sslContextFactory = sslContextFactory;
}
SslValidatingServerConnector(SslBundleKey keyAlias, SslContextFactory sslContextFactory, Server server,
ConnectionFactory... factories) {
super(server, factories);
this.key = keyAlias;
this.sslContextFactory = sslContextFactory;
}
@Override
protected void doStart() throws Exception {
super.doStart();
this.key.assertContainsAlias(this.sslContextFactory.getKeyStore());
}
}
}

View File

@@ -1,23 +0,0 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Reactive and servlet web server implementations backed by Jetty.
*
* @see org.springframework.boot.web.server.servlet.jetty.JettyServletWebServerFactory
* @see org.springframework.boot.web.server.reactive.jetty.JettyReactiveWebServerFactory
*/
package org.springframework.boot.web.server.jetty;

View File

@@ -1,130 +0,0 @@
/*
* Copyright 2012-2025 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.server.reactive.jetty;
import java.net.InetSocketAddress;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.server.ConnectionLimit;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.jetty.ConfigurableJettyWebServerFactory;
import org.springframework.boot.web.server.jetty.ForwardHeadersCustomizer;
import org.springframework.boot.web.server.jetty.JettyServerCustomizer;
import org.springframework.boot.web.server.jetty.JettyWebServer;
import org.springframework.boot.web.server.jetty.JettyWebServerFactory;
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
import org.springframework.boot.web.server.servlet.jetty.JettyServletWebServerFactory;
import org.springframework.http.client.reactive.JettyResourceFactory;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServletHttpHandlerAdapter;
/**
* {@link ReactiveWebServerFactory} that can be used to create {@link JettyWebServer}s.
*
* @author Brian Clozel
* @author Moritz Halbritter
* @since 4.0.0
*/
public class JettyReactiveWebServerFactory extends JettyWebServerFactory
implements ConfigurableJettyWebServerFactory, ConfigurableReactiveWebServerFactory {
private static final Log logger = LogFactory.getLog(JettyReactiveWebServerFactory.class);
private JettyResourceFactory resourceFactory;
/**
* Create a new {@link JettyServletWebServerFactory} instance.
*/
public JettyReactiveWebServerFactory() {
}
/**
* Create a new {@link JettyServletWebServerFactory} that listens for requests using
* the specified port.
* @param port the port to listen on
*/
public JettyReactiveWebServerFactory(int port) {
super(port);
}
@Override
public WebServer getWebServer(HttpHandler httpHandler) {
ServletHttpHandlerAdapter servlet = new ServletHttpHandlerAdapter(httpHandler);
Server server = createJettyServer(servlet);
return new JettyWebServer(server, getPort() >= 0);
}
/**
* Set the {@link JettyResourceFactory} to get the shared resources from.
* @param resourceFactory the server resources
*/
public void setResourceFactory(JettyResourceFactory resourceFactory) {
this.resourceFactory = resourceFactory;
}
protected JettyResourceFactory getResourceFactory() {
return this.resourceFactory;
}
protected Server createJettyServer(ServletHttpHandlerAdapter servlet) {
int port = Math.max(getPort(), 0);
InetSocketAddress address = new InetSocketAddress(getAddress(), port);
Server server = new Server(getThreadPool());
if (this.resourceFactory == null) {
server.addConnector(createConnector(address, server));
}
else {
server.addConnector(createConnector(address, server, this.resourceFactory.getExecutor(),
this.resourceFactory.getScheduler(), this.resourceFactory.getByteBufferPool()));
}
server.setStopTimeout(0);
ServletHolder servletHolder = new ServletHolder(servlet);
servletHolder.setAsyncSupported(true);
ServletContextHandler contextHandler = new ServletContextHandler("/", false, false);
contextHandler.addServlet(servletHolder, "/");
server.setHandler(addHandlerWrappers(contextHandler));
logger.info("Server initialized with port: " + port);
if (this.getMaxConnections() > -1) {
server.addBean(new ConnectionLimit(this.getMaxConnections(), server));
}
if (Ssl.isEnabled(getSsl())) {
customizeSsl(server, address);
}
for (JettyServerCustomizer customizer : getServerCustomizers()) {
customizer.customize(server);
}
if (this.isUseForwardHeaders()) {
new ForwardHeadersCustomizer().customize(server);
}
if (getShutdown() == Shutdown.GRACEFUL) {
StatisticsHandler statisticsHandler = new StatisticsHandler();
statisticsHandler.setHandler(server.getHandler());
server.setHandler(statisticsHandler);
}
return server;
}
}

View File

@@ -1,21 +0,0 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Reactive web server implementation backed by Jetty.
*
*/
package org.springframework.boot.web.server.reactive.jetty;

View File

@@ -1,165 +0,0 @@
/*
* Copyright 2012-2025 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.server.servlet.jetty;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import jakarta.servlet.ServletContainerInitializer;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.springframework.util.ClassUtils;
/**
* Jetty {@link AbstractLifeCycle} to initialize Jasper.
*
* @author Vladimir Tsanev
* @author Phillip Webb
*/
class JasperInitializer extends AbstractLifeCycle {
private static final String[] INITIALIZER_CLASSES = { "org.eclipse.jetty.apache.jsp.JettyJasperInitializer",
"org.apache.jasper.servlet.JasperInitializer" };
private final WebAppContext context;
private final ServletContainerInitializer initializer;
JasperInitializer(WebAppContext context) {
this.context = context;
this.initializer = newInitializer();
}
private ServletContainerInitializer newInitializer() {
for (String className : INITIALIZER_CLASSES) {
try {
Class<?> initializerClass = ClassUtils.forName(className, null);
return (ServletContainerInitializer) initializerClass.getDeclaredConstructor().newInstance();
}
catch (Exception ex) {
// Ignore
}
}
return null;
}
@Override
protected void doStart() throws Exception {
if (this.initializer == null) {
return;
}
if (ClassUtils.isPresent("org.apache.catalina.webresources.TomcatURLStreamHandlerFactory",
getClass().getClassLoader())) {
org.apache.catalina.webresources.TomcatURLStreamHandlerFactory.register();
}
else {
try {
URL.setURLStreamHandlerFactory(new WarUrlStreamHandlerFactory());
}
catch (Error ex) {
// Ignore
}
}
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(this.context.getClassLoader());
try {
this.context.getContext().setExtendedListenerTypes(true);
this.initializer.onStartup(null, this.context.getServletContext());
}
finally {
this.context.getContext().setExtendedListenerTypes(false);
}
}
finally {
Thread.currentThread().setContextClassLoader(classLoader);
}
}
/**
* {@link URLStreamHandlerFactory} to support {@literal war} protocol.
*/
private static final class WarUrlStreamHandlerFactory implements URLStreamHandlerFactory {
@Override
public URLStreamHandler createURLStreamHandler(String protocol) {
if ("war".equals(protocol)) {
return new WarUrlStreamHandler();
}
return null;
}
}
/**
* {@link URLStreamHandler} for {@literal war} protocol compatible with jasper's
* {@link URL urls} produced by
* {@link org.apache.tomcat.util.scan.JarFactory#getJarEntryURL(URL, String)}.
*/
private static final class WarUrlStreamHandler extends URLStreamHandler {
@Override
protected void parseURL(URL u, String spec, int start, int limit) {
String path = "jar:" + spec.substring("war:".length());
int separator = path.indexOf("*/");
if (separator >= 0) {
path = path.substring(0, separator) + "!/" + path.substring(separator + 2);
}
setURL(u, u.getProtocol(), "", -1, null, null, path, null, null);
}
@Override
protected URLConnection openConnection(URL u) throws IOException {
return new WarURLConnection(u);
}
}
/**
* {@link URLConnection} to support {@literal war} protocol.
*/
private static class WarURLConnection extends URLConnection {
private final URLConnection connection;
protected WarURLConnection(URL url) throws IOException {
super(url);
this.connection = new URL(url.getFile()).openConnection();
}
@Override
public void connect() throws IOException {
if (!this.connected) {
this.connection.connect();
this.connected = true;
}
}
@Override
public InputStream getInputStream() throws IOException {
connect();
return this.connection.getInputStream();
}
}
}

View File

@@ -1,39 +0,0 @@
/*
* Copyright 2012-2025 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.server.servlet.jetty;
import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler;
import org.eclipse.jetty.http.HttpMethod;
/**
* Variation of Jetty's {@link ErrorPageErrorHandler} that supports all {@link HttpMethod
* HttpMethods} rather than just {@code GET}, {@code POST} and {@code HEAD}. By default
* Jetty <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=446039">intentionally only
* supports a limited set of HTTP methods</a> for error pages, however, Spring Boot
* prefers Tomcat, Jetty and Undertow to all behave in the same way.
*
* @author Phillip Webb
* @author Christoph Dreis
*/
class JettyEmbeddedErrorHandler extends ErrorPageErrorHandler {
@Override
public boolean errorPageForMethod(String method) {
return true;
}
}

View File

@@ -1,64 +0,0 @@
/*
* Copyright 2012-2025 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.server.servlet.jetty;
import org.eclipse.jetty.ee10.servlet.ServletHandler;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.util.ClassMatcher;
import org.springframework.boot.web.server.jetty.JettyWebServer;
/**
* Jetty {@link WebAppContext} used by {@link JettyWebServer} to support deferred
* initialization.
*
* @author Phillip Webb
*/
class JettyEmbeddedWebAppContext extends WebAppContext {
JettyEmbeddedWebAppContext() {
setHiddenClassMatcher(new ClassMatcher("org.springframework.boot.loader."));
}
@Override
protected ServletHandler newServletHandler() {
return new JettyEmbeddedServletHandler();
}
void deferredInitialize() throws Exception {
JettyEmbeddedServletHandler handler = (JettyEmbeddedServletHandler) getServletHandler();
getContext().call(handler::deferredInitialize, null);
}
@Override
public String getCanonicalNameForTmpDir() {
return super.getCanonicalNameForTmpDir();
}
private static final class JettyEmbeddedServletHandler extends ServletHandler {
@Override
public void initialize() throws Exception {
}
void deferredInitialize() throws Exception {
super.initialize();
}
}
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2012-2025 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.server.servlet.jetty;
import java.util.List;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.springframework.boot.web.server.jetty.JettyWebServer;
/**
* Servlet-specific {@link JettyWebServer}.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
public class JettyServletWebServer extends JettyWebServer {
public JettyServletWebServer(Server server) {
super(server);
}
public JettyServletWebServer(Server server, boolean autoStart) {
super(server, autoStart);
}
@Override
protected void handleDeferredInitialize(Server server) throws Exception {
handleDeferredInitialize(server.getHandlers());
}
protected void handleDeferredInitialize(List<Handler> handlers) throws Exception {
for (Handler handler : handlers) {
handleDeferredInitialize(handler);
}
}
private void handleDeferredInitialize(Handler handler) throws Exception {
if (handler instanceof JettyEmbeddedWebAppContext jettyEmbeddedWebAppContext) {
jettyEmbeddedWebAppContext.deferredInitialize();
}
else if (handler instanceof Handler.Wrapper handlerWrapper) {
handleDeferredInitialize(handlerWrapper.getHandler());
}
else if (handler instanceof Handler.Collection handlerCollection) {
handleDeferredInitialize(handlerCollection.getHandlers());
}
}
}

View File

@@ -1,616 +0,0 @@
/*
* Copyright 2012-2025 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.server.servlet.jetty;
import java.io.File;
import java.net.InetSocketAddress;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EventListener;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import jakarta.servlet.http.Cookie;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jetty.ee10.servlet.ErrorHandler;
import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler;
import org.eclipse.jetty.ee10.servlet.ListenerHolder;
import org.eclipse.jetty.ee10.servlet.ServletHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.ee10.servlet.ServletMapping;
import org.eclipse.jetty.ee10.servlet.SessionHandler;
import org.eclipse.jetty.ee10.servlet.Source;
import org.eclipse.jetty.ee10.webapp.AbstractConfiguration;
import org.eclipse.jetty.ee10.webapp.Configuration;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.ee10.webapp.WebInfConfiguration;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpFields.Mutable;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.MimeTypes.Wrapper;
import org.eclipse.jetty.http.SetCookieParser;
import org.eclipse.jetty.server.ConnectionLimit;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpCookieUtils;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.session.DefaultSessionCache;
import org.eclipse.jetty.session.FileSessionDataStore;
import org.eclipse.jetty.session.SessionConfig;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.util.resource.URLResourceFactory;
import org.springframework.boot.web.server.Cookie.SameSite;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.MimeMappings;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.jetty.ConfigurableJettyWebServerFactory;
import org.springframework.boot.web.server.jetty.ForwardHeadersCustomizer;
import org.springframework.boot.web.server.jetty.JettyServerCustomizer;
import org.springframework.boot.web.server.jetty.JettyWebServer;
import org.springframework.boot.web.server.jetty.JettyWebServerFactory;
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.server.servlet.ContextPath;
import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier;
import org.springframework.boot.web.server.servlet.DocumentRoot;
import org.springframework.boot.web.server.servlet.ServletContextInitializer;
import org.springframework.boot.web.server.servlet.ServletContextInitializers;
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
import org.springframework.boot.web.server.servlet.ServletWebServerSettings;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* {@link ServletWebServerFactory} that can be used to create a {@link JettyWebServer}.
* Can be initialized using Spring's {@link ServletContextInitializer}s or Jetty
* {@link Configuration}s.
* <p>
* Unless explicitly configured otherwise this factory will create servers that listen for
* HTTP requests on port 8080.
*
* @author Phillip Webb
* @author Dave Syer
* @author Andrey Hihlovskiy
* @author Andy Wilkinson
* @author Eddú Meléndez
* @author Venil Noronha
* @author Henri Kerola
* @author Moritz Halbritter
* @author Onur Kagan Ozcan
* @since 4.0.0
* @see #setPort(int)
* @see #setConfigurations(Collection)
* @see JettyWebServer
*/
public class JettyServletWebServerFactory extends JettyWebServerFactory
implements ConfigurableJettyWebServerFactory, ConfigurableServletWebServerFactory, ResourceLoaderAware {
private static final Log logger = LogFactory.getLog(JettyServletWebServerFactory.class);
private final ServletWebServerSettings settings = new ServletWebServerSettings();
private ResourceLoader resourceLoader;
/**
* Create a new {@link JettyServletWebServerFactory} instance.
*/
public JettyServletWebServerFactory() {
}
/**
* Create a new {@link JettyServletWebServerFactory} that listens for requests using
* the specified port.
* @param port the port to listen on
*/
public JettyServletWebServerFactory(int port) {
super(port);
}
/**
* Create a new {@link JettyServletWebServerFactory} with the specified context path
* and port.
* @param contextPath the root context path
* @param port the port to listen on
*/
public JettyServletWebServerFactory(String contextPath, int port) {
super(port);
getSettings().setContextPath(ContextPath.of(contextPath));
}
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
JettyEmbeddedWebAppContext context = new JettyEmbeddedWebAppContext();
context.getContext().getServletContext().setExtendedListenerTypes(true);
int port = Math.max(getPort(), 0);
InetSocketAddress address = new InetSocketAddress(getAddress(), port);
Server server = createServer(address);
context.setServer(server);
configureWebAppContext(context, initializers);
server.setHandler(addHandlerWrappers(context));
logger.info("Server initialized with port: " + port);
if (this.getMaxConnections() > -1) {
server.addBean(new ConnectionLimit(this.getMaxConnections(), server.getConnectors()));
}
if (Ssl.isEnabled(getSsl())) {
customizeSsl(server, address);
}
for (JettyServerCustomizer customizer : getServerCustomizers()) {
customizer.customize(server);
}
if (this.isUseForwardHeaders()) {
new ForwardHeadersCustomizer().customize(server);
}
if (getShutdown() == Shutdown.GRACEFUL) {
StatisticsHandler statisticsHandler = new StatisticsHandler();
statisticsHandler.setHandler(server.getHandler());
server.setHandler(statisticsHandler);
}
return getJettyWebServer(server);
}
private Server createServer(InetSocketAddress address) {
Server server = new Server(getThreadPool());
server.setConnectors(new Connector[] { createConnector(address, server) });
server.setStopTimeout(0);
MimeTypes.Mutable mimeTypes = server.getMimeTypes();
for (MimeMappings.Mapping mapping : getSettings().getMimeMappings()) {
mimeTypes.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
}
return server;
}
@Override
protected Handler addHandlerWrappers(Handler handler) {
handler = super.addHandlerWrappers(handler);
if (!CollectionUtils.isEmpty(getSettings().getCookieSameSiteSuppliers())) {
handler = applyWrapper(handler, new SuppliedSameSiteCookieHandlerWrapper(getSessionCookieName(),
getSettings().getCookieSameSiteSuppliers()));
}
return handler;
}
private String getSessionCookieName() {
String name = getSettings().getSession().getCookie().getName();
return (name != null) ? name : SessionConfig.__DefaultSessionCookie;
}
/**
* Configure the given Jetty {@link WebAppContext} for use.
* @param context the context to configure
* @param initializers the set of initializers to apply
*/
protected final void configureWebAppContext(WebAppContext context, ServletContextInitializer... initializers) {
Assert.notNull(context, "'context' must not be null");
context.clearAliasChecks();
if (this.resourceLoader != null) {
context.setClassLoader(this.resourceLoader.getClassLoader());
}
String contextPath = getSettings().getContextPath().toString();
context.setContextPath(StringUtils.hasLength(contextPath) ? contextPath : "/");
context.setDisplayName(getSettings().getDisplayName());
configureDocumentRoot(context);
if (getSettings().isRegisterDefaultServlet()) {
addDefaultServlet(context);
}
if (shouldRegisterJspServlet()) {
addJspServlet(context);
context.addBean(new JasperInitializer(context), true);
}
addLocaleMappings(context);
ServletContextInitializers initializersToUse = ServletContextInitializers.from(this.settings, initializers);
Configuration[] configurations = getWebAppContextConfigurations(context, initializersToUse);
context.setConfigurations(configurations);
context.setThrowUnavailableOnStartupException(true);
configureSession(context);
context.setTempDirectory(getTempDirectory(context));
postProcessWebAppContext(context);
}
private boolean shouldRegisterJspServlet() {
return this.settings.getJsp() != null && this.settings.getJsp().getRegistered()
&& ClassUtils.isPresent(this.settings.getJsp().getClassName(), getClass().getClassLoader());
}
private void configureSession(WebAppContext context) {
SessionHandler handler = context.getSessionHandler();
SameSite sessionSameSite = getSettings().getSession().getCookie().getSameSite();
if (sessionSameSite != null && sessionSameSite != SameSite.OMITTED) {
handler.setSameSite(HttpCookie.SameSite.valueOf(sessionSameSite.name()));
}
Duration sessionTimeout = getSettings().getSession().getTimeout();
handler.setMaxInactiveInterval(isNegative(sessionTimeout) ? -1 : (int) sessionTimeout.getSeconds());
if (getSettings().getSession().isPersistent()) {
DefaultSessionCache cache = new DefaultSessionCache(handler);
FileSessionDataStore store = new FileSessionDataStore();
store.setStoreDir(getSettings().getSession().getSessionStoreDirectory().getValidDirectory(true));
cache.setSessionDataStore(store);
handler.setSessionCache(cache);
}
}
private boolean isNegative(Duration sessionTimeout) {
return sessionTimeout == null || sessionTimeout.isNegative();
}
private void addLocaleMappings(WebAppContext context) {
getSettings().getLocaleCharsetMappings()
.forEach((locale, charset) -> context.addLocaleEncoding(locale.toString(), charset.toString()));
}
private File getTempDirectory(WebAppContext context) {
String temp = System.getProperty("java.io.tmpdir");
return (temp != null) ? new File(temp, getTempDirectoryPrefix(context) + UUID.randomUUID()) : null;
}
@SuppressWarnings("removal")
private String getTempDirectoryPrefix(WebAppContext context) {
try {
return ((JettyEmbeddedWebAppContext) context).getCanonicalNameForTmpDir();
}
catch (Throwable ex) {
return WebInfConfiguration.getCanonicalNameForWebAppTmpDir(context);
}
}
private void configureDocumentRoot(WebAppContext handler) {
DocumentRoot documentRoot = new DocumentRoot(logger);
documentRoot.setDirectory(this.settings.getDocumentRoot());
File root = documentRoot.getValidDirectory();
File docBase = (root != null) ? root : createTempDir("jetty-docbase");
try {
ResourceFactory resourceFactory = handler.getResourceFactory();
List<Resource> resources = new ArrayList<>();
Resource rootResource = (docBase.isDirectory()
? resourceFactory.newResource(docBase.getCanonicalFile().toURI())
: resourceFactory.newJarFileResource(docBase.toURI()));
resources.add((root != null) ? new LoaderHidingResource(rootResource, rootResource) : rootResource);
URLResourceFactory urlResourceFactory = new URLResourceFactory();
for (URL resourceJarUrl : getSettings().getStaticResourceUrls()) {
Resource resource = createResource(resourceJarUrl, resourceFactory, urlResourceFactory);
if (resource != null) {
resources.add(resource);
}
}
handler.setBaseResource(ResourceFactory.combine(resources));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private Resource createResource(URL url, ResourceFactory resourceFactory, URLResourceFactory urlResourceFactory)
throws Exception {
if ("file".equals(url.getProtocol())) {
File file = new File(url.toURI());
if (file.isFile()) {
return resourceFactory.newResource("jar:" + url + "!/META-INF/resources/");
}
if (file.isDirectory()) {
return resourceFactory.newResource(url).resolve("META-INF/resources/");
}
}
return urlResourceFactory.newResource(url + "META-INF/resources/");
}
/**
* Add Jetty's {@code DefaultServlet} to the given {@link WebAppContext}.
* @param context the jetty {@link WebAppContext}
*/
protected final void addDefaultServlet(WebAppContext context) {
Assert.notNull(context, "'context' must not be null");
ServletHolder holder = new ServletHolder();
holder.setName("default");
holder.setClassName("org.eclipse.jetty.ee10.servlet.DefaultServlet");
holder.setInitParameter("dirAllowed", "false");
holder.setInitOrder(1);
context.getServletHandler().addServletWithMapping(holder, "/");
ServletMapping servletMapping = context.getServletHandler().getServletMapping("/");
servletMapping.setFromDefaultDescriptor(true);
}
/**
* Add Jetty's {@code JspServlet} to the given {@link WebAppContext}.
* @param context the jetty {@link WebAppContext}
*/
protected final void addJspServlet(WebAppContext context) {
Assert.notNull(context, "'context' must not be null");
ServletHolder holder = new ServletHolder();
holder.setName("jsp");
holder.setClassName(this.settings.getJsp().getClassName());
holder.setInitParameter("fork", "false");
holder.setInitParameters(this.settings.getJsp().getInitParameters());
holder.setInitOrder(3);
context.getServletHandler().addServlet(holder);
ServletMapping mapping = new ServletMapping();
mapping.setServletName("jsp");
mapping.setPathSpecs(new String[] { "*.jsp", "*.jspx" });
context.getServletHandler().addServletMapping(mapping);
}
/**
* Return the Jetty {@link Configuration}s that should be applied to the server.
* @param webAppContext the Jetty {@link WebAppContext}
* @param initializers the {@link ServletContextInitializer}s to apply
* @return configurations to apply
*/
protected Configuration[] getWebAppContextConfigurations(WebAppContext webAppContext,
ServletContextInitializers initializers) {
List<Configuration> configurations = new ArrayList<>();
configurations.add(getServletContextInitializerConfiguration(webAppContext, initializers));
configurations.add(getErrorPageConfiguration());
configurations.add(getMimeTypeConfiguration());
configurations.add(new WebListenersConfiguration(getSettings().getWebListenerClassNames()));
configurations.addAll(getConfigurations());
return configurations.toArray(new Configuration[0]);
}
/**
* Create a configuration object that adds error handlers.
* @return a configuration object for adding error pages
*/
private Configuration getErrorPageConfiguration() {
return new AbstractConfiguration(new AbstractConfiguration.Builder()) {
@Override
public void configure(WebAppContext context) throws Exception {
JettyEmbeddedErrorHandler errorHandler = new JettyEmbeddedErrorHandler();
context.setErrorHandler(errorHandler);
addJettyErrorPages(errorHandler, getErrorPages());
}
};
}
/**
* Create a configuration object that adds mime type mappings.
* @return a configuration object for adding mime type mappings
*/
private Configuration getMimeTypeConfiguration() {
return new AbstractConfiguration(new AbstractConfiguration.Builder()) {
@Override
public void configure(WebAppContext context) throws Exception {
MimeTypes.Wrapper mimeTypes = (Wrapper) context.getMimeTypes();
mimeTypes.setWrapped(new MimeTypes(null));
for (MimeMappings.Mapping mapping : getSettings().getMimeMappings()) {
mimeTypes.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
}
}
};
}
/**
* Return a Jetty {@link Configuration} that will invoke the specified
* {@link ServletContextInitializer}s. By default this method will return a
* {@link ServletContextInitializerConfiguration}.
* @param webAppContext the Jetty {@link WebAppContext}
* @param initializers the {@link ServletContextInitializer}s to apply
* @return the {@link Configuration} instance
*/
protected Configuration getServletContextInitializerConfiguration(WebAppContext webAppContext,
ServletContextInitializers initializers) {
return new ServletContextInitializerConfiguration(initializers);
}
/**
* Post process the Jetty {@link WebAppContext} before it's used with the Jetty
* Server. Subclasses can override this method to apply additional processing to the
* {@link WebAppContext}.
* @param webAppContext the Jetty {@link WebAppContext}
*/
protected void postProcessWebAppContext(WebAppContext webAppContext) {
}
/**
* Factory method called to create the {@link JettyWebServer}. Subclasses can override
* this method to return a different {@link JettyWebServer} or apply additional
* processing to the Jetty server.
* @param server the Jetty server.
* @return a new {@link JettyWebServer} instance
*/
protected JettyWebServer getJettyWebServer(Server server) {
return new JettyServletWebServer(server, getPort() >= 0);
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
private void addJettyErrorPages(ErrorHandler errorHandler, Collection<ErrorPage> errorPages) {
if (errorHandler instanceof ErrorPageErrorHandler handler) {
for (ErrorPage errorPage : errorPages) {
if (errorPage.isGlobal()) {
handler.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, errorPage.getPath());
}
else {
if (errorPage.getExceptionName() != null) {
handler.addErrorPage(errorPage.getExceptionName(), errorPage.getPath());
}
else {
handler.addErrorPage(errorPage.getStatusCode(), errorPage.getPath());
}
}
}
}
}
@Override
public ServletWebServerSettings getSettings() {
return this.settings;
}
/**
* {@link AbstractConfiguration} to apply {@code @WebListener} classes.
*/
private static class WebListenersConfiguration extends AbstractConfiguration {
private final Set<String> classNames;
WebListenersConfiguration(Set<String> webListenerClassNames) {
super(new AbstractConfiguration.Builder());
this.classNames = webListenerClassNames;
}
@Override
public void configure(WebAppContext context) throws Exception {
ServletHandler servletHandler = context.getServletHandler();
for (String className : this.classNames) {
configure(context, servletHandler, className);
}
}
private void configure(WebAppContext context, ServletHandler servletHandler, String className)
throws ClassNotFoundException {
ListenerHolder holder = servletHandler.newListenerHolder(new Source(Source.Origin.ANNOTATION, className));
holder.setHeldClass(loadClass(context, className));
servletHandler.addListener(holder);
}
@SuppressWarnings("unchecked")
private Class<? extends EventListener> loadClass(WebAppContext context, String className)
throws ClassNotFoundException {
ClassLoader classLoader = context.getClassLoader();
classLoader = (classLoader != null) ? classLoader : getClass().getClassLoader();
return (Class<? extends EventListener>) classLoader.loadClass(className);
}
}
/**
* {@link Handler.Wrapper} to apply {@link CookieSameSiteSupplier supplied}
* {@link SameSite} cookie values.
*/
private static class SuppliedSameSiteCookieHandlerWrapper extends Handler.Wrapper {
private static final SetCookieParser setCookieParser = SetCookieParser.newInstance();
private final String sessionCookieName;
private final List<? extends CookieSameSiteSupplier> suppliers;
SuppliedSameSiteCookieHandlerWrapper(String sessionCookieName,
List<? extends CookieSameSiteSupplier> suppliers) {
this.sessionCookieName = sessionCookieName;
this.suppliers = suppliers;
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception {
SuppliedSameSiteCookieResponse wrappedResponse = new SuppliedSameSiteCookieResponse(request, response);
return super.handle(request, wrappedResponse, callback);
}
private class SuppliedSameSiteCookieResponse extends Response.Wrapper {
private final HttpFields.Mutable wrappedHeaders;
SuppliedSameSiteCookieResponse(Request request, Response wrapped) {
super(request, wrapped);
this.wrappedHeaders = new SuppliedSameSiteCookieHeaders(
request.getConnectionMetaData().getHttpConfiguration().getResponseCookieCompliance(),
wrapped.getHeaders());
}
@Override
public Mutable getHeaders() {
return this.wrappedHeaders;
}
}
private class SuppliedSameSiteCookieHeaders extends HttpFields.Mutable.Wrapper {
private final CookieCompliance compliance;
SuppliedSameSiteCookieHeaders(CookieCompliance compliance, HttpFields.Mutable fields) {
super(fields);
this.compliance = compliance;
}
@Override
public HttpField onAddField(HttpField field) {
return (field.getHeader() != HttpHeader.SET_COOKIE) ? field : onAddSetCookieField(field);
}
private HttpField onAddSetCookieField(HttpField field) {
HttpCookie cookie = setCookieParser.parse(field.getValue());
if (cookie == null || isSessionCookie(cookie)) {
return field;
}
SameSite sameSite = getSameSite(cookie);
if (sameSite == null) {
return field;
}
HttpCookie updatedCookie = buildCookieWithUpdatedSameSite(cookie, sameSite);
return new HttpCookieUtils.SetCookieHttpField(updatedCookie, this.compliance);
}
private boolean isSessionCookie(HttpCookie cookie) {
return SuppliedSameSiteCookieHandlerWrapper.this.sessionCookieName.equals(cookie.getName());
}
private HttpCookie buildCookieWithUpdatedSameSite(HttpCookie cookie, SameSite sameSite) {
return HttpCookie.build(cookie)
.sameSite(org.eclipse.jetty.http.HttpCookie.SameSite.from(sameSite.name()))
.build();
}
private SameSite getSameSite(HttpCookie cookie) {
return getSameSite(asServletCookie(cookie));
}
private SameSite getSameSite(Cookie cookie) {
return SuppliedSameSiteCookieHandlerWrapper.this.suppliers.stream()
.map((supplier) -> supplier.getSameSite(cookie))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
private Cookie asServletCookie(HttpCookie cookie) {
Cookie servletCookie = new Cookie(cookie.getName(), cookie.getValue());
cookie.getAttributes().forEach(servletCookie::setAttribute);
return servletCookie;
}
}
}
}

View File

@@ -1,196 +0,0 @@
/*
* Copyright 2012-2025 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.server.servlet.jetty;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.function.Consumer;
import org.eclipse.jetty.util.resource.CombinedResource;
import org.eclipse.jetty.util.resource.Resource;
/**
* A custom {@link Resource} that hides Spring Boot's loader classes, preventing them from
* being served over HTTP.
*
* @author Andy Wilkinson
*/
final class LoaderHidingResource extends Resource {
private static final String LOADER_RESOURCE_PATH_PREFIX = "/org/springframework/boot/";
private final Path loaderBasePath;
private final Resource base;
private final Resource delegate;
LoaderHidingResource(Resource base, Resource delegate) {
this.base = base;
this.delegate = delegate;
this.loaderBasePath = base.getPath().getFileSystem().getPath("/", "org", "springframework", "boot");
}
@Override
public void forEach(Consumer<? super Resource> action) {
this.delegate.forEach(action);
}
@Override
public Path getPath() {
return this.delegate.getPath();
}
@Override
public boolean isContainedIn(Resource r) {
return this.delegate.isContainedIn(r);
}
@Override
public Iterator<Resource> iterator() {
if (this.delegate instanceof CombinedResource) {
return list().iterator();
}
return List.<Resource>of(this).iterator();
}
@Override
public boolean equals(Object obj) {
return this.delegate.equals(obj);
}
@Override
public int hashCode() {
return this.delegate.hashCode();
}
@Override
public boolean exists() {
return this.delegate.exists();
}
@Override
public Spliterator<Resource> spliterator() {
return this.delegate.spliterator();
}
@Override
public boolean isDirectory() {
return this.delegate.isDirectory();
}
@Override
public boolean isReadable() {
return this.delegate.isReadable();
}
@Override
public Instant lastModified() {
return this.delegate.lastModified();
}
@Override
public long length() {
return this.delegate.length();
}
@Override
public URI getURI() {
return this.delegate.getURI();
}
@Override
public String getName() {
return this.delegate.getName();
}
@Override
public String getFileName() {
return this.delegate.getFileName();
}
@Override
public InputStream newInputStream() throws IOException {
return this.delegate.newInputStream();
}
@Override
@SuppressWarnings({ "deprecation", "removal" })
public ReadableByteChannel newReadableByteChannel() throws IOException {
return this.delegate.newReadableByteChannel();
}
@Override
public List<Resource> list() {
return asLoaderHidingResources(this.delegate.list());
}
private boolean nonLoaderResource(Resource resource) {
return !resource.getPath().startsWith(this.loaderBasePath);
}
private List<Resource> asLoaderHidingResources(Collection<Resource> resources) {
return resources.stream().filter(this::nonLoaderResource).map(this::asLoaderHidingResource).toList();
}
private Resource asLoaderHidingResource(Resource resource) {
return (resource instanceof LoaderHidingResource) ? resource : new LoaderHidingResource(this.base, resource);
}
@Override
public Resource resolve(String subUriPath) {
if (subUriPath.startsWith(LOADER_RESOURCE_PATH_PREFIX)) {
return null;
}
Resource resolved = this.delegate.resolve(subUriPath);
return (resolved != null) ? new LoaderHidingResource(this.base, resolved) : null;
}
@Override
public boolean isAlias() {
return this.delegate.isAlias();
}
@Override
public URI getRealURI() {
return this.delegate.getRealURI();
}
@Override
public void copyTo(Path destination) throws IOException {
this.delegate.copyTo(destination);
}
@Override
public Collection<Resource> getAllResources() {
return asLoaderHidingResources(this.delegate.getAllResources());
}
@Override
public String toString() {
return this.delegate.toString();
}
}

View File

@@ -1,72 +0,0 @@
/*
* Copyright 2012-2025 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.server.servlet.jetty;
import jakarta.servlet.ServletException;
import org.eclipse.jetty.ee10.webapp.AbstractConfiguration;
import org.eclipse.jetty.ee10.webapp.Configuration;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.springframework.boot.web.server.servlet.ServletContextInitializer;
import org.springframework.boot.web.server.servlet.ServletContextInitializers;
import org.springframework.util.Assert;
/**
* Jetty {@link Configuration} that calls {@link ServletContextInitializer}s.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class ServletContextInitializerConfiguration extends AbstractConfiguration {
private final ServletContextInitializers initializers;
/**
* Create a new {@link ServletContextInitializerConfiguration}.
* @param initializers the initializers that should be invoked
*/
ServletContextInitializerConfiguration(ServletContextInitializers initializers) {
super(new AbstractConfiguration.Builder());
Assert.notNull(initializers, "'initializers' must not be null");
this.initializers = initializers;
}
@Override
public void configure(WebAppContext context) throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(context.getClassLoader());
try {
callInitializers(context);
}
finally {
Thread.currentThread().setContextClassLoader(classLoader);
}
}
private void callInitializers(WebAppContext context) throws ServletException {
try {
context.getContext().setExtendedListenerTypes(true);
for (ServletContextInitializer initializer : this.initializers) {
initializer.onStartup(context.getServletContext());
}
}
finally {
context.getContext().setExtendedListenerTypes(false);
}
}
}

View File

@@ -1,20 +0,0 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Servlet web server implementation backed by Jetty.
*/
package org.springframework.boot.web.server.servlet.jetty;

View File

@@ -1,34 +0,0 @@
/*
* Copyright 2012-2025 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.server.jetty;
/**
* Helper class to provide public access to package-private methods for testing purposes.
*
* @author Andy Wilkinson
*/
public final class JettyAccess {
private JettyAccess() {
}
public static String getStartedLogMessage(JettyWebServer jettyWebServer) {
return jettyWebServer.getStartedLogMessage();
}
}

View File

@@ -1,147 +0,0 @@
/*
* Copyright 2012-2025 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.server.jetty;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.OS;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
import org.springframework.boot.testsupport.junit.DisabledOnOs;
import org.springframework.boot.testsupport.ssl.MockPkcs11Security;
import org.springframework.boot.testsupport.ssl.MockPkcs11SecurityProvider;
import org.springframework.boot.web.server.Http2;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServerSslBundle;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for {@link SslServerCustomizer}.
*
* @author Andy Wilkinson
* @author Cyril Dangerville
* @author Scott Frederick
*/
@MockPkcs11Security
class SslServerCustomizerTests {
@Test
@SuppressWarnings("rawtypes")
@WithPackageResources("test.jks")
void whenHttp2IsNotEnabledServerConnectorHasSslAndHttpConnectionFactories() {
Server server = createCustomizedServer();
assertThat(server.getConnectors()).hasSize(1);
List<ConnectionFactory> factories = new ArrayList<>(server.getConnectors()[0].getConnectionFactories());
assertThat(factories).extracting((factory) -> (Class) factory.getClass())
.containsExactly(SslConnectionFactory.class, HttpConnectionFactory.class);
}
@Test
@SuppressWarnings("rawtypes")
@WithPackageResources("test.jks")
@DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64",
disabledReason = "conscrypt doesn't support Linux/macOS aarch64, see https://github.com/google/conscrypt/issues/1051")
void whenHttp2IsEnabledServerConnectorsHasSslAlpnH2AndHttpConnectionFactories() {
Http2 http2 = new Http2();
http2.setEnabled(true);
Server server = createCustomizedServer(http2);
assertThat(server.getConnectors()).hasSize(1);
List<ConnectionFactory> factories = new ArrayList<>(server.getConnectors()[0].getConnectionFactories());
assertThat(factories).extracting((factory) -> (Class) factory.getClass())
.containsExactly(SslConnectionFactory.class, ALPNServerConnectionFactory.class,
HTTP2ServerConnectionFactory.class, HttpConnectionFactory.class);
}
@Test
@WithPackageResources("test.jks")
@DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64",
disabledReason = "conscrypt doesn't support Linux/macOS aarch64, see https://github.com/google/conscrypt/issues/1051")
void alpnConnectionFactoryHasNullDefaultProtocolToAllowNegotiationToHttp11() {
Http2 http2 = new Http2();
http2.setEnabled(true);
Server server = createCustomizedServer(http2);
assertThat(server.getConnectors()).hasSize(1);
List<ConnectionFactory> factories = new ArrayList<>(server.getConnectors()[0].getConnectionFactories());
assertThat(((ALPNServerConnectionFactory) factories.get(1)).getDefaultProtocol()).isNull();
}
@Test
void configureSslWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() {
Ssl ssl = new Ssl();
assertThatIllegalStateException().isThrownBy(() -> {
SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl));
customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth());
}).withMessageContaining("SSL is enabled but no trust material is configured");
}
@Test
@WithPackageResources("test.jks")
void configureSslWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsException() {
Ssl ssl = new Ssl();
ssl.setKeyStoreType("PKCS11");
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
ssl.setKeyStore("classpath:test.jks");
ssl.setKeyPassword("password");
assertThatIllegalStateException().isThrownBy(() -> {
SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl));
customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth());
}).withMessageContaining("must be empty or null for PKCS11 hardware key stores");
}
@Test
void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() {
Ssl ssl = new Ssl();
ssl.setKeyStoreType("PKCS11");
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
ssl.setKeyStorePassword("1234");
assertThatNoException().isThrownBy(() -> {
SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl));
customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth());
});
}
private Server createCustomizedServer() {
return createCustomizedServer(new Http2());
}
private Server createCustomizedServer(Http2 http2) {
Ssl ssl = new Ssl();
ssl.setKeyStore("classpath:test.jks");
return createCustomizedServer(ssl, http2);
}
private Server createCustomizedServer(Ssl ssl, Http2 http2) {
Server server = new Server();
new SslServerCustomizer(http2, new InetSocketAddress(0), ssl.getClientAuth(), WebServerSslBundle.get(ssl))
.customize(server);
return server;
}
}

View File

@@ -1,183 +0,0 @@
/*
* Copyright 2012-2025 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.server.reactive.jetty;
import java.net.ConnectException;
import java.net.InetAddress;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import org.awaitility.Awaitility;
import org.eclipse.jetty.server.ConnectionLimit;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.Ssl.ServerNameSslBundle;
import org.springframework.boot.web.server.jetty.JettyAccess;
import org.springframework.boot.web.server.jetty.JettyServerCustomizer;
import org.springframework.boot.web.server.jetty.JettyWebServer;
import org.springframework.boot.web.server.reactive.AbstractReactiveWebServerFactoryTests;
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JettyReactiveWebServerFactory} and {@link JettyWebServer}.
*
* @author Brian Clozel
* @author Madhura Bhave
* @author Moritz Halbritter
*/
class JettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests {
@Override
protected JettyReactiveWebServerFactory getFactory() {
return new JettyReactiveWebServerFactory(0);
}
@Test
@Override
@Disabled("Jetty 12 does not support User-Agent-based compression")
// TODO Is this true with Jetty 12?
protected void noCompressionForUserAgent() {
}
@Test
void setNullServerCustomizersShouldThrowException() {
JettyReactiveWebServerFactory factory = getFactory();
assertThatIllegalArgumentException().isThrownBy(() -> factory.setServerCustomizers(null))
.withMessageContaining("'customizers' must not be null");
}
@Test
void addNullServerCustomizersShouldThrowException() {
JettyReactiveWebServerFactory factory = getFactory();
assertThatIllegalArgumentException()
.isThrownBy(() -> factory.addServerCustomizers((JettyServerCustomizer[]) null))
.withMessageContaining("'customizers' must not be null");
}
@Test
void jettyCustomizersShouldBeInvoked() {
HttpHandler handler = mock(HttpHandler.class);
JettyReactiveWebServerFactory factory = getFactory();
JettyServerCustomizer[] configurations = new JettyServerCustomizer[4];
Arrays.setAll(configurations, (i) -> mock(JettyServerCustomizer.class));
factory.setServerCustomizers(Arrays.asList(configurations[0], configurations[1]));
factory.addServerCustomizers(configurations[2], configurations[3]);
this.webServer = factory.getWebServer(handler);
InOrder ordered = inOrder((Object[]) configurations);
for (JettyServerCustomizer configuration : configurations) {
ordered.verify(configuration).customize(any(Server.class));
}
}
@Test
void specificIPAddressNotReverseResolved() throws Exception {
JettyReactiveWebServerFactory factory = getFactory();
InetAddress localhost = InetAddress.getLocalHost();
factory.setAddress(InetAddress.getByAddress(localhost.getAddress()));
this.webServer = factory.getWebServer(mock(HttpHandler.class));
this.webServer.start();
Connector connector = ((JettyWebServer) this.webServer).getServer().getConnectors()[0];
assertThat(((ServerConnector) connector).getHost()).isEqualTo(localhost.getHostAddress());
}
@Test
void useForwardedHeaders() {
JettyReactiveWebServerFactory factory = getFactory();
factory.setUseForwardHeaders(true);
assertForwardHeaderIsUsed(factory);
}
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() {
JettyReactiveWebServerFactory factory = getFactory();
factory.setShutdown(Shutdown.GRACEFUL);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
WebClient webClient = getWebClient(this.webServer.getPort()).build();
this.webServer.shutDownGracefully((result) -> {
});
Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> {
blockingHandler.stopBlocking();
try {
webClient.get().retrieve().toBodilessEntity().block();
return false;
}
catch (RuntimeException ex) {
return ex.getCause() instanceof ConnectException;
}
});
this.webServer.stop();
}
@Test
void shouldApplyMaxConnections() {
JettyReactiveWebServerFactory factory = getFactory();
factory.setMaxConnections(1);
this.webServer = factory.getWebServer(new EchoHandler());
Server server = ((JettyWebServer) this.webServer).getServer();
ConnectionLimit connectionLimit = server.getBean(ConnectionLimit.class);
assertThat(connectionLimit).isNotNull();
assertThat(connectionLimit.getMaxConnections()).isOne();
}
@Test
void sslServerNameBundlesConfigurationThrowsException() {
Ssl ssl = new Ssl();
ssl.setBundle("test");
List<ServerNameSslBundle> bundles = List.of(new ServerNameSslBundle("first", "test1"),
new ServerNameSslBundle("second", "test2"));
ssl.setServerNameBundles(bundles);
JettyReactiveWebServerFactory factory = getFactory();
factory.setSsl(ssl);
assertThatIllegalStateException().isThrownBy(() -> this.webServer = factory.getWebServer(new EchoHandler()))
.withMessageContaining("Server name SSL bundles are not supported with Jetty");
}
@Override
protected String startedLogMessage() {
return JettyAccess.getStartedLogMessage((JettyWebServer) this.webServer);
}
@Override
protected void addConnector(int port, ConfigurableReactiveWebServerFactory factory) {
((JettyReactiveWebServerFactory) factory).addServerCustomizers((server) -> {
ServerConnector connector = new ServerConnector(server);
connector.setPort(port);
server.addConnector(connector);
});
}
}

View File

@@ -1,625 +0,0 @@
/*
* Copyright 2012-2025 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.server.servlet.jetty;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EventListener;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.ServletRegistration.Dynamic;
import org.apache.hc.client5.http.HttpHostConnectException;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.jasper.servlet.JspServlet;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.awaitility.Awaitility;
import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.ee10.webapp.AbstractConfiguration;
import org.eclipse.jetty.ee10.webapp.Configuration;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.server.AbstractConnector;
import org.eclipse.jetty.server.ConnectionLimit;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.ClassMatcher;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.boot.testsupport.classpath.resources.ResourcePath;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.web.server.Compression;
import org.springframework.boot.web.server.GracefulShutdownResult;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.Ssl.ServerNameSslBundle;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.boot.web.server.jetty.JettyAccess;
import org.springframework.boot.web.server.jetty.JettyServerCustomizer;
import org.springframework.boot.web.server.jetty.JettyWebServer;
import org.springframework.boot.web.server.servlet.AbstractServletWebServerFactoryTests;
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JettyServletWebServerFactory}.
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
* @author Henri Kerola
* @author Moritz Halbritter
* @author Onur Kagan Ozcan
*/
class JettyServletWebServerFactoryTests extends AbstractServletWebServerFactoryTests {
@Override
protected JettyServletWebServerFactory getFactory() {
JettyServletWebServerFactory factory = new JettyServletWebServerFactory(0);
factory.addServerCustomizers((server) -> {
for (Connector connector : server.getConnectors()) {
if (connector instanceof ServerConnector serverConnector) {
// TODO Set the shutdown idle timeout in main code?
serverConnector.setShutdownIdleTimeout(10000);
}
}
});
return factory;
}
@Override
protected void addConnector(int port, ConfigurableServletWebServerFactory factory) {
((JettyServletWebServerFactory) factory).addServerCustomizers((server) -> {
ServerConnector connector = new ServerConnector(server);
connector.setPort(port);
server.addConnector(connector);
});
}
@Override
protected JspServlet getJspServlet() throws Exception {
WebAppContext context = findWebAppContext((JettyWebServer) this.webServer);
ServletHolder holder = context.getServletHandler().getServlet("jsp");
if (holder == null) {
return null;
}
holder.start();
holder.initialize();
return (JspServlet) holder.getServlet();
}
@Override
protected Map<String, String> getActualMimeMappings() {
WebAppContext context = findWebAppContext((JettyWebServer) this.webServer);
return context.getMimeTypes().getMimeMap();
}
@Override
protected Charset getCharset(Locale locale) {
WebAppContext context = findWebAppContext((JettyWebServer) this.webServer);
String charsetName = context.getLocaleEncoding(locale);
return (charsetName != null) ? Charset.forName(charsetName) : null;
}
@Override
protected void handleExceptionCausedByBlockedPortOnPrimaryConnector(RuntimeException ex, int blockedPort) {
assertThat(ex).isInstanceOf(PortInUseException.class);
assertThat(((PortInUseException) ex).getPort()).isEqualTo(blockedPort);
}
@Override
protected void handleExceptionCausedByBlockedPortOnSecondaryConnector(RuntimeException ex, int blockedPort) {
handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, blockedPort);
}
@Test
@Override
@Disabled("Jetty 12 does not support User-Agent-based compression")
protected void noCompressionForUserAgent() {
}
@Test
@Override
@Disabled("Jetty 12 does not support SSL session tracking")
protected void sslSessionTracking() {
}
@Test
void contextPathIsLoggedOnStartupWhenCompressionIsEnabled(CapturedOutput output) {
ConfigurableServletWebServerFactory factory = getFactory();
factory.setContextPath("/custom");
Compression compression = new Compression();
compression.setEnabled(true);
factory.setCompression(compression);
this.webServer = factory.getWebServer(exampleServletRegistration());
this.webServer.start();
assertThat(output).containsOnlyOnce("with context path '/custom'");
}
@Test
void jettyConfigurations() throws Exception {
JettyServletWebServerFactory factory = getFactory();
Configuration[] configurations = new Configuration[] { mockConfiguration(Configuration1.class),
mockConfiguration(Configuration2.class), mockConfiguration(Configuration3.class),
mockConfiguration(Configuration4.class) };
factory.setConfigurations(Arrays.asList(configurations[0], configurations[1]));
factory.addConfigurations(configurations[2], configurations[3]);
this.webServer = factory.getWebServer();
InOrder ordered = inOrder((Object[]) configurations);
for (Configuration configuration : configurations) {
ordered.verify(configuration).configure(any(WebAppContext.class));
}
}
Configuration mockConfiguration(Class<? extends Configuration> type) {
Configuration mock = mock(type);
ClassMatcher classMatcher = new ClassMatcher();
given(mock.getProtectedClasses()).willReturn(classMatcher);
given(mock.getHiddenClasses()).willReturn(classMatcher);
return mock;
}
@Test
void jettyCustomizations() {
JettyServletWebServerFactory factory = getFactory();
JettyServerCustomizer[] configurations = new JettyServerCustomizer[4];
Arrays.setAll(configurations, (i) -> mock(JettyServerCustomizer.class));
factory.setServerCustomizers(Arrays.asList(configurations[0], configurations[1]));
factory.addServerCustomizers(configurations[2], configurations[3]);
this.webServer = factory.getWebServer();
InOrder ordered = inOrder((Object[]) configurations);
for (JettyServerCustomizer configuration : configurations) {
ordered.verify(configuration).customize(any(Server.class));
}
}
@Test
void sessionTimeout() {
ConfigurableServletWebServerFactory factory = getFactory();
factory.getSettings().getSession().setTimeout(Duration.ofSeconds(10));
assertTimeout(factory, 10);
}
@Test
void sessionTimeoutInMinutes() {
ConfigurableServletWebServerFactory factory = getFactory();
factory.getSettings().getSession().setTimeout(Duration.ofMinutes(1));
assertTimeout(factory, 60);
}
@Test
@WithPackageResources("test.jks")
void sslCiphersConfiguration(@ResourcePath("test.jks") String keyStore) {
Ssl ssl = new Ssl();
ssl.setKeyStore(keyStore);
ssl.setKeyStorePassword("secret");
ssl.setKeyPassword("password");
ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" });
JettyServletWebServerFactory factory = getFactory();
factory.setSsl(ssl);
this.webServer = factory.getWebServer();
this.webServer.start();
JettyWebServer jettyWebServer = (JettyWebServer) this.webServer;
ServerConnector connector = (ServerConnector) jettyWebServer.getServer().getConnectors()[0];
SslConnectionFactory connectionFactory = connector.getConnectionFactory(SslConnectionFactory.class);
SslContextFactory sslContextFactory = extractSslContextFactory(connectionFactory);
assertThat(sslContextFactory.getIncludeCipherSuites()).containsExactly("ALPHA", "BRAVO", "CHARLIE");
assertThat(sslContextFactory.getExcludeCipherSuites()).isEmpty();
}
@Test
void destroyCalledWithoutStart() {
JettyServletWebServerFactory factory = getFactory();
this.webServer = factory.getWebServer(exampleServletRegistration());
this.webServer.destroy();
Server server = ((JettyWebServer) this.webServer).getServer();
assertThat(server.isStopped()).isTrue();
}
@Test
@WithPackageResources("test.jks")
void sslEnabledMultiProtocolsConfiguration(@ResourcePath("test.jks") String keyStore) {
JettyServletWebServerFactory factory = getFactory();
factory.setSsl(getSslSettings(keyStore, "TLSv1.1", "TLSv1.2"));
this.webServer = factory.getWebServer();
this.webServer.start();
JettyWebServer jettyWebServer = (JettyWebServer) this.webServer;
ServerConnector connector = (ServerConnector) jettyWebServer.getServer().getConnectors()[0];
SslConnectionFactory connectionFactory = connector.getConnectionFactory(SslConnectionFactory.class);
SslContextFactory sslContextFactory = extractSslContextFactory(connectionFactory);
assertThat(sslContextFactory.getIncludeProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
}
@Test
@WithPackageResources("test.jks")
void sslEnabledProtocolsConfiguration(@ResourcePath("test.jks") String keyStore) {
JettyServletWebServerFactory factory = getFactory();
factory.setSsl(getSslSettings(keyStore, "TLSv1.1"));
this.webServer = factory.getWebServer();
this.webServer.start();
JettyWebServer jettyWebServer = (JettyWebServer) this.webServer;
ServerConnector connector = (ServerConnector) jettyWebServer.getServer().getConnectors()[0];
SslConnectionFactory connectionFactory = connector.getConnectionFactory(SslConnectionFactory.class);
SslContextFactory sslContextFactory = extractSslContextFactory(connectionFactory);
assertThat(sslContextFactory.getIncludeProtocols()).containsExactly("TLSv1.1");
}
@Test
void sslServerNameBundlesConfigurationThrowsException() {
Ssl ssl = new Ssl();
ssl.setBundle("test");
List<ServerNameSslBundle> bundles = List.of(new ServerNameSslBundle("first", "test1"),
new ServerNameSslBundle("second", "test2"));
ssl.setServerNameBundles(bundles);
JettyServletWebServerFactory factory = getFactory();
factory.setSsl(ssl);
assertThatIllegalStateException().isThrownBy(() -> this.webServer = factory.getWebServer())
.withMessageContaining("Server name SSL bundles are not supported with Jetty");
}
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
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception {
ConfigurableServletWebServerFactory factory = getFactory();
factory.setShutdown(Shutdown.GRACEFUL);
BlockingServlet blockingServlet = new BlockingServlet();
this.webServer = factory.getWebServer((context) -> {
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
registration.addMapping("/blocking");
registration.setAsyncSupported(true);
});
this.webServer.start();
int port = this.webServer.getPort();
Future<Object> request = initiateGetRequest(port, "/blocking");
blockingServlet.awaitQueue();
this.webServer.shutDownGracefully((result) -> {
});
Future<Object> unconnectableRequest = initiateGetRequest(port, "/");
blockingServlet.admitOne();
Object response = request.get();
assertThat(response).isInstanceOf(HttpResponse.class);
assertThat(unconnectableRequest.get()).isInstanceOf(HttpHostConnectException.class);
this.webServer.stop();
}
@Test
void whenServerIsShuttingDownGracefullyThenResponseToRequestOnIdleConnectionWillHaveAConnectionCloseHeader()
throws Exception {
ConfigurableServletWebServerFactory factory = getFactory();
factory.setShutdown(Shutdown.GRACEFUL);
BlockingServlet blockingServlet = new BlockingServlet();
this.webServer = factory.getWebServer((context) -> {
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
registration.addMapping("/blocking");
registration.setAsyncSupported(true);
});
this.webServer.start();
int port = this.webServer.getPort();
HttpClient client = HttpClients.createMinimal();
Future<Object> request = initiateGetRequest(client, port, "/blocking");
blockingServlet.awaitQueue();
blockingServlet.admitOne();
Object response = request.get();
assertThat(response).isInstanceOf(HttpResponse.class);
assertThat(((HttpResponse) response).getCode()).isEqualTo(200);
assertThat(((HttpResponse) response).getFirstHeader("Connection")).isNull();
this.webServer.shutDownGracefully((result) -> {
});
request = initiateGetRequest(client, port, "/blocking");
blockingServlet.awaitQueue();
blockingServlet.admitOne();
response = request.get();
assertThat(response).isInstanceOf(HttpResponse.class);
assertThat(((HttpResponse) response).getCode()).isEqualTo(200);
assertThat(((HttpResponse) response).getFirstHeader("Connection")).isNotNull()
.extracting(Header::getValue)
.isEqualTo("close");
this.webServer.stop();
}
@Test
void whenARequestCompletesAfterGracefulShutdownHasBegunThenItHasAConnectionCloseHeader()
throws InterruptedException, ExecutionException {
ConfigurableServletWebServerFactory factory = getFactory();
factory.setShutdown(Shutdown.GRACEFUL);
BlockingServlet blockingServlet = new BlockingServlet();
this.webServer = factory.getWebServer((context) -> {
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
registration.addMapping("/blocking");
registration.setAsyncSupported(true);
});
this.webServer.start();
int port = this.webServer.getPort();
Future<Object> request = initiateGetRequest(port, "/blocking");
blockingServlet.awaitQueue();
AtomicReference<GracefulShutdownResult> result = new AtomicReference<>();
this.webServer.shutDownGracefully(result::set);
blockingServlet.admitOne();
Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> GracefulShutdownResult.IDLE == result.get());
Object requestResult = request.get();
assertThat(requestResult).isInstanceOf(HttpResponse.class);
assertThat(((HttpResponse) requestResult).getFirstHeader("Connection").getValue()).isEqualTo("close");
}
private Ssl getSslSettings(String keyStore, String... enabledProtocols) {
Ssl ssl = new Ssl();
ssl.setKeyStore(keyStore);
ssl.setKeyStorePassword("secret");
ssl.setKeyPassword("password");
ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" });
ssl.setEnabledProtocols(enabledProtocols);
return ssl;
}
private void assertTimeout(ConfigurableServletWebServerFactory factory, int expected) {
this.webServer = factory.getWebServer();
JettyWebServer jettyWebServer = (JettyWebServer) this.webServer;
WebAppContext webAppContext = findWebAppContext(jettyWebServer);
int actual = webAppContext.getSessionHandler().getMaxInactiveInterval();
assertThat(actual).isEqualTo(expected);
}
@Test
void wrappedHandlers() throws Exception {
JettyServletWebServerFactory factory = getFactory();
factory.setServerCustomizers(Collections.singletonList((server) -> {
Handler handler = server.getHandler();
Handler.Wrapper wrapper = new Handler.Wrapper();
wrapper.setHandler(handler);
server.setHandler(wrapper);
}));
this.webServer = factory.getWebServer(exampleServletRegistration());
this.webServer.start();
assertThat(getResponse(getLocalUrl("/hello"))).isEqualTo("Hello World");
}
@Test
@WithPackageResources("test.jks")
void basicSslClasspathKeyStore() throws Exception {
testBasicSslWithKeyStore("classpath:test.jks");
}
@Test
void useForwardHeaders() throws Exception {
JettyServletWebServerFactory factory = getFactory();
factory.setUseForwardHeaders(true);
assertForwardHeaderIsUsed(factory);
}
@Test
void defaultThreadPool() {
JettyServletWebServerFactory factory = getFactory();
factory.setThreadPool(null);
assertThat(factory.getThreadPool()).isNull();
this.webServer = factory.getWebServer();
assertThat(((JettyWebServer) this.webServer).getServer().getThreadPool()).isNotNull();
}
@Test
void customThreadPool() {
JettyServletWebServerFactory factory = getFactory();
ThreadPool threadPool = mock(ThreadPool.class);
factory.setThreadPool(threadPool);
this.webServer = factory.getWebServer();
assertThat(((JettyWebServer) this.webServer).getServer().getThreadPool()).isSameAs(threadPool);
}
@Test
void startFailsWhenThreadPoolIsTooSmall() {
JettyServletWebServerFactory factory = getFactory();
factory.addServerCustomizers((server) -> {
QueuedThreadPool threadPool = server.getBean(QueuedThreadPool.class);
threadPool.setMaxThreads(2);
threadPool.setMinThreads(2);
});
assertThatExceptionOfType(WebServerException.class).isThrownBy(factory.getWebServer()::start)
.withCauseInstanceOf(IllegalStateException.class);
}
@Test
void specificIPAddressNotReverseResolved() throws Exception {
JettyServletWebServerFactory factory = getFactory();
InetAddress localhost = InetAddress.getLocalHost();
factory.setAddress(InetAddress.getByAddress(localhost.getAddress()));
this.webServer = factory.getWebServer();
this.webServer.start();
Connector connector = ((JettyWebServer) this.webServer).getServer().getConnectors()[0];
assertThat(((ServerConnector) connector).getHost()).isEqualTo(localhost.getHostAddress());
}
@Test
@WithPackageResources("test.jks")
void specificIPAddressWithSslIsNotReverseResolved() throws Exception {
JettyServletWebServerFactory factory = getFactory();
InetAddress localhost = InetAddress.getLocalHost();
factory.setAddress(InetAddress.getByAddress(localhost.getAddress()));
Ssl ssl = new Ssl();
ssl.setKeyStore("classpath:test.jks");
ssl.setKeyStorePassword("secret");
ssl.setKeyPassword("password");
factory.setSsl(ssl);
this.webServer = factory.getWebServer();
this.webServer.start();
Connector connector = ((JettyWebServer) this.webServer).getServer().getConnectors()[0];
assertThat(((ServerConnector) connector).getHost()).isEqualTo(localhost.getHostAddress());
}
@Test
void faultyListenerCausesStartFailure() {
JettyServletWebServerFactory factory = getFactory();
factory.addServerCustomizers((JettyServerCustomizer) (server) -> {
Collection<WebAppContext> contexts = server.getBeans(WebAppContext.class);
EventListener eventListener = new ServletContextListener() {
@Override
public void contextInitialized(ServletContextEvent event) {
throw new RuntimeException();
}
@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();
try {
jettyWebServer.start();
}
finally {
QueuedThreadPool threadPool = (QueuedThreadPool) jettyWebServer.getServer().getThreadPool();
assertThat(threadPool.isRunning()).isFalse();
}
});
}
@Test
void errorHandlerCanBeOverridden() {
JettyServletWebServerFactory factory = getFactory();
factory.addConfigurations(new AbstractConfiguration(new AbstractConfiguration.Builder()) {
@Override
public void configure(WebAppContext context) throws Exception {
context.setErrorHandler(new CustomErrorHandler());
}
});
JettyWebServer jettyWebServer = (JettyWebServer) factory.getWebServer();
WebAppContext context = findWebAppContext(jettyWebServer);
assertThat(context.getErrorHandler()).isInstanceOf(CustomErrorHandler.class);
}
@Test
void shouldApplyMaxConnections() {
JettyServletWebServerFactory factory = getFactory();
factory.setMaxConnections(1);
this.webServer = factory.getWebServer();
Server server = ((JettyWebServer) this.webServer).getServer();
ConnectionLimit connectionLimit = server.getBean(ConnectionLimit.class);
assertThat(connectionLimit).isNotNull();
assertThat(connectionLimit.getMaxConnections()).isOne();
}
@Test
void shouldApplyMaxConnectionsToConnectors() {
JettyServletWebServerFactory factory = getFactory();
factory.setMaxConnections(1);
this.webServer = factory.getWebServer();
Server server = ((JettyWebServer) this.webServer).getServer();
assertThat(server.getConnectors()).isEmpty();
ConnectionLimit connectionLimit = server.getBean(ConnectionLimit.class);
assertThat(connectionLimit).extracting("_connectors")
.asInstanceOf(InstanceOfAssertFactories.list(AbstractConnector.class))
.hasSize(1);
}
@Override
protected String startedLogMessage() {
return JettyAccess.getStartedLogMessage((JettyWebServer) this.webServer);
}
private WebAppContext findWebAppContext(JettyWebServer webServer) {
return findWebAppContext(webServer.getServer().getHandler());
}
private WebAppContext findWebAppContext(Handler handler) {
if (handler instanceof WebAppContext webAppContext) {
return webAppContext;
}
if (handler instanceof Handler.Wrapper wrapper) {
return findWebAppContext(wrapper.getHandler());
}
throw new IllegalStateException("No WebAppContext found");
}
private static final class CustomErrorHandler extends ErrorPageErrorHandler {
}
interface Configuration1 extends Configuration {
}
interface Configuration2 extends Configuration {
}
interface Configuration3 extends Configuration {
}
interface Configuration4 extends Configuration {
}
}

View File

@@ -1,104 +0,0 @@
/*
* Copyright 2012-2025 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.server.servlet.jetty;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystems;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import org.eclipse.jetty.util.resource.PathResourceFactory;
import org.eclipse.jetty.util.resource.Resource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link LoaderHidingResource}.
*
* @author Andy Wilkinson
*/
class LoaderHidingResourceTests {
@Test
void listHidesLoaderResources(@TempDir File temp) throws IOException {
URI warUri = createExampleWar(temp);
Resource resource = new PathResourceFactory().newResource(warUri);
LoaderHidingResource loaderHidingResource = new LoaderHidingResource(resource, resource);
assertThat(deepList(loaderHidingResource)).hasOnlyElementsOfType(LoaderHidingResource.class)
.extracting(Resource::getName)
.contains("/assets/image.jpg")
.doesNotContain("/org/springframework/boot/Loader.class");
}
@Test
void getAllResourcesHidesLoaderResources(@TempDir File temp) throws IOException {
URI warUri = createExampleWar(temp);
Resource resource = new PathResourceFactory().newResource(warUri);
LoaderHidingResource loaderHidingResource = new LoaderHidingResource(resource, resource);
Collection<Resource> allResources = loaderHidingResource.getAllResources();
assertThat(allResources).hasOnlyElementsOfType(LoaderHidingResource.class)
.extracting(Resource::getName)
.contains("/assets/image.jpg")
.doesNotContain("/org/springframework/boot/Loader.class");
}
@Test
void resolveHidesLoaderResources(@TempDir File temp) throws IOException {
URI warUri = createExampleWar(temp);
Resource resource = new PathResourceFactory().newResource(warUri);
LoaderHidingResource loaderHidingResource = new LoaderHidingResource(resource, resource);
assertThat(loaderHidingResource.resolve("/assets/image.jpg").exists()).isTrue();
assertThat(loaderHidingResource.resolve("/assets/image.jpg")).isInstanceOf(LoaderHidingResource.class);
assertThat(loaderHidingResource.resolve("/assets/non-existent.jpg").exists()).isFalse();
assertThat(loaderHidingResource.resolve("/assets/non-existent.jpg")).isInstanceOf(LoaderHidingResource.class);
assertThat(loaderHidingResource.resolve("/org/springframework/boot/Loader.class")).isNull();
}
private URI createExampleWar(File temp) throws IOException {
File exampleWarFile = new File(temp, "example.war");
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(exampleWarFile))) {
out.putNextEntry(new ZipEntry("org/"));
out.putNextEntry(new ZipEntry("org/springframework/"));
out.putNextEntry(new ZipEntry("org/springframework/boot/"));
out.putNextEntry(new ZipEntry("org/springframework/boot/Loader.class"));
out.putNextEntry(new ZipEntry("assets/"));
out.putNextEntry(new ZipEntry("assets/image.jpg"));
}
URI warUri = URI.create("jar:" + exampleWarFile.toURI() + "!/");
FileSystems.newFileSystem(warUri, Collections.emptyMap());
return warUri;
}
private List<Resource> deepList(Resource resource) {
List<Resource> all = new ArrayList<>();
for (Resource listed : resource.list()) {
all.add(listed);
all.addAll(deepList(listed));
}
return all;
}
}