Consolidate Undertow WebServers and simplify their constructors

Closes gh-21391

Co-authored-by: Phillip Webb <pwebb@pivotal.io>
pull/21440/head
Andy Wilkinson 5 years ago
parent 0d00947735
commit c42571ba40

@ -0,0 +1,121 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.embedded.undertow;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.accesslog.AccessLogHandler;
import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.Xnio;
import org.xnio.XnioWorker;
import org.springframework.util.Assert;
/**
* A {@link HttpHandlerFactory} for an {@link AccessLogHandler}.
*
* @author Andy Wilkinson
*/
class AccessLogHttpHandlerFactory implements HttpHandlerFactory {
private final File directory;
private final String pattern;
private final String prefix;
private final String suffix;
private final boolean rotate;
AccessLogHttpHandlerFactory(File directory, String pattern, String prefix, String suffix, boolean rotate) {
this.directory = directory;
this.pattern = pattern;
this.prefix = prefix;
this.suffix = suffix;
this.rotate = rotate;
}
@Override
public HttpHandler getHandler(HttpHandler next) {
try {
createAccessLogDirectoryIfNecessary();
XnioWorker worker = createWorker();
String baseName = (this.prefix != null) ? this.prefix : "access_log.";
String formatString = (this.pattern != null) ? this.pattern : "common";
return new ClosableAccessLogHandler(next, worker,
new DefaultAccessLogReceiver(worker, this.directory, baseName, this.suffix, this.rotate),
formatString);
}
catch (IOException ex) {
throw new IllegalStateException("Failed to create AccessLogHandler", ex);
}
}
private void createAccessLogDirectoryIfNecessary() {
Assert.state(this.directory != null, "Access log directory is not set");
if (!this.directory.isDirectory() && !this.directory.mkdirs()) {
throw new IllegalStateException("Failed to create access log directory '" + this.directory + "'");
}
}
private XnioWorker createWorker() throws IOException {
Xnio xnio = Xnio.getInstance(Undertow.class.getClassLoader());
return xnio.createWorker(OptionMap.builder().set(Options.THREAD_DAEMON, true).getMap());
}
/**
* {@link Closeable} variant of {@link AccessLogHandler}.
*/
private static class ClosableAccessLogHandler extends AccessLogHandler implements Closeable {
private final DefaultAccessLogReceiver accessLogReceiver;
private final XnioWorker worker;
ClosableAccessLogHandler(HttpHandler next, XnioWorker worker, DefaultAccessLogReceiver accessLogReceiver,
String formatString) {
super(next, accessLogReceiver, formatString, Undertow.class.getClassLoader());
this.worker = worker;
this.accessLogReceiver = accessLogReceiver;
}
@Override
public void close() throws IOException {
try {
this.accessLogReceiver.close();
this.worker.shutdown();
this.worker.awaitTermination(30, TimeUnit.SECONDS);
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}

@ -37,30 +37,28 @@ import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* Configure the HTTP compression on an Undertow {@link HttpHandler}.
* {@link HttpHandlerFactory} that adds a compression handler.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
final class UndertowCompressionConfigurer {
class CompressionHttpHandlerFactory implements HttpHandlerFactory {
private UndertowCompressionConfigurer() {
private final Compression compression;
CompressionHttpHandlerFactory(Compression compression) {
this.compression = compression;
}
/**
* Optionally wrap the given {@link HttpHandler} for HTTP compression support.
* @param compression the HTTP compression configuration
* @param httpHandler the HTTP handler to wrap
* @return the wrapped HTTP handler if compression is enabled, or the handler itself
*/
static HttpHandler configureCompression(Compression compression, HttpHandler httpHandler) {
if (compression == null || !compression.getEnabled()) {
return httpHandler;
@Override
public HttpHandler getHandler(HttpHandler next) {
if (!this.compression.getEnabled()) {
return next;
}
ContentEncodingRepository repository = new ContentEncodingRepository();
repository.addEncodingHandler("gzip", new GzipEncodingProvider(), 50,
Predicates.and(getCompressionPredicates(compression)));
return new EncodingHandler(repository).setNext(httpHandler);
Predicates.and(getCompressionPredicates(this.compression)));
return new EncodingHandler(repository).setNext(next);
}
private static Predicate[] getCompressionPredicates(Compression compression) {
@ -76,6 +74,9 @@ final class UndertowCompressionConfigurer {
return predicates.toArray(new Predicate[0]);
}
/**
* Predicate used to match specific mime types.
*/
private static class CompressibleMimeTypePredicate implements Predicate {
private final List<MimeType> mimeTypes;

@ -17,6 +17,7 @@
package org.springframework.boot.web.embedded.undertow;
import java.io.File;
import java.util.Collection;
import io.undertow.Undertow.Builder;
@ -32,6 +33,14 @@ import org.springframework.boot.web.server.ConfigurableWebServerFactory;
*/
public interface ConfigurableUndertowWebServerFactory extends ConfigurableWebServerFactory {
/**
* Set {@link UndertowBuilderCustomizer}s that should be applied to the Undertow
* {@link Builder}. Calling this method will replace any existing customizers.
* @param customizers the customizers to set
* @since 2.3.0
*/
void setBuilderCustomizers(Collection<? extends UndertowBuilderCustomizer> customizers);
/**
* Add {@link UndertowBuilderCustomizer}s that should be used to customize the
* Undertow {@link Builder}.

@ -0,0 +1,95 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.embedded.undertow;
import java.io.Closeable;
import java.io.IOException;
import javax.servlet.ServletException;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.api.DeploymentManager;
import org.springframework.util.Assert;
/**
* {@link HttpHandlerFactory} that for a {@link DeploymentManager}.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
class DeploymentManagerHttpHandlerFactory implements HttpHandlerFactory {
private final DeploymentManager deploymentManager;
DeploymentManagerHttpHandlerFactory(DeploymentManager deploymentManager) {
this.deploymentManager = deploymentManager;
}
@Override
public HttpHandler getHandler(HttpHandler next) {
Assert.state(next == null, "DeploymentManagerHttpHandlerFactory must be first");
return new DeploymentManagerHandler(this.deploymentManager);
}
DeploymentManager getDeploymentManager() {
return this.deploymentManager;
}
/**
* {@link HttpHandler} that delegates to a {@link DeploymentManager}.
*/
static class DeploymentManagerHandler implements HttpHandler, Closeable {
private final DeploymentManager deploymentManager;
private final HttpHandler handler;
DeploymentManagerHandler(DeploymentManager deploymentManager) {
this.deploymentManager = deploymentManager;
try {
this.handler = deploymentManager.start();
}
catch (ServletException ex) {
throw new RuntimeException(ex);
}
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
this.handler.handleRequest(exchange);
}
@Override
public void close() throws IOException {
try {
this.deploymentManager.stop();
this.deploymentManager.undeploy();
}
catch (ServletException ex) {
throw new RuntimeException(ex);
}
}
DeploymentManager getDeploymentManager() {
return this.deploymentManager;
}
}
}

@ -18,6 +18,7 @@ package org.springframework.boot.web.embedded.undertow;
import java.time.Duration;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -25,34 +26,33 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.boot.web.server.GracefulShutdown;
/**
* {@link GracefulShutdown} for Undertow.
* A {@link GracefulShutdownHandler} with support for our own {@link GracefulShutdown}
* interface.
*
* @author Andy Wilkinson
*/
class UndertowGracefulShutdown implements GracefulShutdown {
class GracefulShutdownHttpHandler extends GracefulShutdownHandler implements GracefulShutdown {
private static final Log logger = LogFactory.getLog(UndertowGracefulShutdown.class);
private static final Log logger = LogFactory.getLog(GracefulShutdownHttpHandler.class);
private final GracefulShutdownHandler gracefulShutdownHandler;
private final Duration period;
private final Duration gracePeriod;
private volatile boolean shuttingDown;
UndertowGracefulShutdown(GracefulShutdownHandler gracefulShutdownHandler, Duration period) {
this.gracefulShutdownHandler = gracefulShutdownHandler;
this.period = period;
GracefulShutdownHttpHandler(HttpHandler next, Duration period) {
super(next);
this.gracePeriod = period;
}
@Override
public boolean shutDownGracefully() {
logger.info("Commencing graceful shutdown, allowing up to " + this.period.getSeconds()
logger.info("Commencing graceful shutdown, allowing up to " + this.gracePeriod.getSeconds()
+ "s for active requests to complete");
this.gracefulShutdownHandler.shutdown();
shutdown();
this.shuttingDown = true;
boolean graceful = false;
try {
graceful = this.gracefulShutdownHandler.awaitShutdown(this.period.toMillis());
graceful = awaitShutdown(this.gracePeriod.toMillis());
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();

@ -0,0 +1,47 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.embedded.undertow;
import java.io.Closeable;
import io.undertow.server.HttpHandler;
import org.springframework.boot.web.server.GracefulShutdown;
/**
* Factory used by {@link UndertowServletWebServer} to add {@link HttpHandler
* HttpHandlers}. Instances returned from this factory may optionally implement the
* following interfaces:
* <ul>
* <li>{@link Closeable} - if they wish to be closed just before server stops.</li>
* <li>{@link GracefulShutdown} - if they wish to manage graceful shutdown.</li>
* </ul>
*
* @author Phillip Webb
* @since 2.3.0
*/
@FunctionalInterface
public interface HttpHandlerFactory {
/**
* Create the {@link HttpHandler} instance that should be added.
* @param next the next handler in the chain
* @return the new HTTP handler instance
*/
HttpHandler getHandler(HttpHandler next);
}

@ -16,34 +16,16 @@
package org.springframework.boot.web.embedded.undertow;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.List;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.UndertowOptions;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import io.undertow.server.handlers.accesslog.AccessLogHandler;
import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.Xnio;
import org.xnio.XnioWorker;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.boot.web.server.WebServer;
import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter;
import org.springframework.util.Assert;
/**
* {@link ReactiveWebServerFactory} that can be used to create {@link UndertowWebServer}s.
@ -54,29 +36,7 @@ import org.springframework.util.Assert;
public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerFactory
implements ConfigurableUndertowWebServerFactory {
private Set<UndertowBuilderCustomizer> builderCustomizers = new LinkedHashSet<>();
private Integer bufferSize;
private Integer ioThreads;
private Integer workerThreads;
private Boolean directBuffers;
private File accessLogDirectory;
private String accessLogPattern;
private String accessLogPrefix;
private String accessLogSuffix;
private boolean accessLogEnabled = false;
private boolean accessLogRotate = true;
private boolean useForwardHeaders;
private UndertowWebServerFactoryDelegate delegate = new UndertowWebServerFactoryDelegate();
/**
* Create a new {@link UndertowReactiveWebServerFactory} instance.
@ -94,220 +54,93 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
}
@Override
public WebServer getWebServer(org.springframework.http.server.reactive.HttpHandler httpHandler) {
Undertow.Builder builder = createBuilder(getPort());
HttpHandler handler = new UndertowHttpHandlerAdapter(httpHandler);
if (this.useForwardHeaders) {
handler = Handlers.proxyPeerAddress(handler);
}
handler = UndertowCompressionConfigurer.configureCompression(getCompression(), handler);
Closeable closeable = null;
if (isAccessLogEnabled()) {
AccessLogHandlerConfiguration accessLogHandlerConfiguration = configureAccessLogHandler(handler);
closeable = accessLogHandlerConfiguration.closeable;
handler = accessLogHandlerConfiguration.accessLogHandler;
}
GracefulShutdown gracefulShutdown = null;
GracefulShutdownHandler gracefulShutdownHandler = Handlers.gracefulShutdown(handler);
Duration gracePeriod = getShutdown().getGracePeriod();
if (gracePeriod != null) {
gracefulShutdown = new UndertowGracefulShutdown(gracefulShutdownHandler, gracePeriod);
handler = gracefulShutdownHandler;
}
else {
gracefulShutdown = GracefulShutdown.IMMEDIATE;
}
builder.setHandler(handler);
return new UndertowWebServer(builder, getPort() >= 0, closeable, gracefulShutdown);
public void setBuilderCustomizers(Collection<? extends UndertowBuilderCustomizer> customizers) {
this.delegate.setBuilderCustomizers(customizers);
}
private Undertow.Builder createBuilder(int port) {
Undertow.Builder builder = Undertow.builder();
if (this.bufferSize != null) {
builder.setBufferSize(this.bufferSize);
}
if (this.ioThreads != null) {
builder.setIoThreads(this.ioThreads);
}
if (this.workerThreads != null) {
builder.setWorkerThreads(this.workerThreads);
}
if (this.directBuffers != null) {
builder.setDirectBuffers(this.directBuffers);
}
if (getSsl() != null && getSsl().isEnabled()) {
customizeSsl(builder);
}
else {
builder.addHttpListener(port, getListenAddress());
}
builder.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 0);
for (UndertowBuilderCustomizer customizer : this.builderCustomizers) {
customizer.customize(builder);
}
return builder;
@Override
public void addBuilderCustomizers(UndertowBuilderCustomizer... customizers) {
this.delegate.addBuilderCustomizers(customizers);
}
private AccessLogHandlerConfiguration configureAccessLogHandler(HttpHandler handler) {
try {
createAccessLogDirectoryIfNecessary();
XnioWorker worker = createWorker();
String prefix = (this.accessLogPrefix != null) ? this.accessLogPrefix : "access_log.";
DefaultAccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver(worker, this.accessLogDirectory,
prefix, this.accessLogSuffix, this.accessLogRotate);
String formatString = ((this.accessLogPattern != null) ? this.accessLogPattern : "common");
AccessLogHandler accessLogHandler = new AccessLogHandler(handler, accessLogReceiver, formatString,
Undertow.class.getClassLoader());
return new AccessLogHandlerConfiguration(accessLogHandler, () -> {
try {
accessLogReceiver.close();
worker.shutdown();
worker.awaitTermination(30, TimeUnit.SECONDS);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
});
}
catch (IOException ex) {
throw new IllegalStateException("Failed to create AccessLogHandler", ex);
}
/**
* Returns a mutable collection of the {@link UndertowBuilderCustomizer}s that will be
* applied to the Undertow {@link io.undertow.Undertow.Builder Builder}.
* @return the customizers that will be applied
*/
public Collection<UndertowBuilderCustomizer> getBuilderCustomizers() {
return this.delegate.getBuilderCustomizers();
}
private void createAccessLogDirectoryIfNecessary() {
Assert.state(this.accessLogDirectory != null, "Access log directory is not set");
if (!this.accessLogDirectory.isDirectory() && !this.accessLogDirectory.mkdirs()) {
throw new IllegalStateException("Failed to create access log directory '" + this.accessLogDirectory + "'");
}
@Override
public void setBufferSize(Integer bufferSize) {
this.delegate.setBufferSize(bufferSize);
}
private XnioWorker createWorker() throws IOException {
Xnio xnio = Xnio.getInstance(Undertow.class.getClassLoader());
return xnio.createWorker(OptionMap.builder().set(Options.THREAD_DAEMON, true).getMap());
@Override
public void setIoThreads(Integer ioThreads) {
this.delegate.setIoThreads(ioThreads);
}
private void customizeSsl(Undertow.Builder builder) {
new SslBuilderCustomizer(getPort(), getAddress(), getSsl(), getSslStoreProvider()).customize(builder);
if (getHttp2() != null) {
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, getHttp2().isEnabled());
@Override
public void setWorkerThreads(Integer workerThreads) {
this.delegate.setWorkerThreads(workerThreads);
}
@Override
public void setUseDirectBuffers(Boolean directBuffers) {
this.delegate.setUseDirectBuffers(directBuffers);
}
private String getListenAddress() {
if (getAddress() == null) {
return "0.0.0.0";
@Override
public void setUseForwardHeaders(boolean useForwardHeaders) {
this.delegate.setUseForwardHeaders(useForwardHeaders);
}
return getAddress().getHostAddress();
protected final boolean isUseForwardHeaders() {
return this.delegate.isUseForwardHeaders();
}
@Override
public void setAccessLogDirectory(File accessLogDirectory) {
this.accessLogDirectory = accessLogDirectory;
this.delegate.setAccessLogDirectory(accessLogDirectory);
}
@Override
public void setAccessLogPattern(String accessLogPattern) {
this.accessLogPattern = accessLogPattern;
this.delegate.setAccessLogPattern(accessLogPattern);
}
@Override
public void setAccessLogPrefix(String accessLogPrefix) {
this.accessLogPrefix = accessLogPrefix;
this.delegate.setAccessLogPrefix(accessLogPrefix);
}
@Override
public void setAccessLogSuffix(String accessLogSuffix) {
this.accessLogSuffix = accessLogSuffix;
this.delegate.setAccessLogSuffix(accessLogSuffix);
}
public boolean isAccessLogEnabled() {
return this.accessLogEnabled;
return this.delegate.isAccessLogEnabled();
}
@Override
public void setAccessLogEnabled(boolean accessLogEnabled) {
this.accessLogEnabled = accessLogEnabled;
this.delegate.setAccessLogEnabled(accessLogEnabled);
}
@Override
public void setAccessLogRotate(boolean accessLogRotate) {
this.accessLogRotate = accessLogRotate;
}
protected final boolean isUseForwardHeaders() {
return this.useForwardHeaders;
}
@Override
public void setUseForwardHeaders(boolean useForwardHeaders) {
this.useForwardHeaders = useForwardHeaders;
}
@Override
public void setBufferSize(Integer bufferSize) {
this.bufferSize = bufferSize;
this.delegate.setAccessLogRotate(accessLogRotate);
}
@Override
public void setIoThreads(Integer ioThreads) {
this.ioThreads = ioThreads;
}
@Override
public void setWorkerThreads(Integer workerThreads) {
this.workerThreads = workerThreads;
}
@Override
public void setUseDirectBuffers(Boolean directBuffers) {
this.directBuffers = directBuffers;
}
/**
* Set {@link UndertowBuilderCustomizer}s that should be applied to the Undertow
* {@link io.undertow.Undertow.Builder Builder}. Calling this method will replace any
* existing customizers.
* @param customizers the customizers to set
*/
public void setBuilderCustomizers(Collection<? extends UndertowBuilderCustomizer> customizers) {
Assert.notNull(customizers, "Customizers must not be null");
this.builderCustomizers = new LinkedHashSet<>(customizers);
}
/**
* Returns a mutable collection of the {@link UndertowBuilderCustomizer}s that will be
* applied to the Undertow {@link io.undertow.Undertow.Builder Builder}.
* @return the customizers that will be applied
*/
public Collection<UndertowBuilderCustomizer> getBuilderCustomizers() {
return this.builderCustomizers;
}
/**
* Add {@link UndertowBuilderCustomizer}s that should be used to customize the
* Undertow {@link io.undertow.Undertow.Builder Builder}.
* @param customizers the customizers to add
*/
@Override
public void addBuilderCustomizers(UndertowBuilderCustomizer... customizers) {
Assert.notNull(customizers, "Customizers must not be null");
this.builderCustomizers.addAll(Arrays.asList(customizers));
}
private static final class AccessLogHandlerConfiguration {
private final AccessLogHandler accessLogHandler;
private final Closeable closeable;
private AccessLogHandlerConfiguration(AccessLogHandler accessLogHandler, Closeable closeable) {
this.accessLogHandler = accessLogHandler;
this.closeable = closeable;
}
public WebServer getWebServer(org.springframework.http.server.reactive.HttpHandler httpHandler) {
Undertow.Builder builder = this.delegate.createBuilder(this);
List<HttpHandlerFactory> httpHandlerFactories = this.delegate.createHttpHandlerFactories(this,
(next) -> new UndertowHttpHandlerAdapter(httpHandler));
return new UndertowWebServer(builder, httpHandlerFactories, getPort() >= 0);
}
}

@ -16,31 +16,13 @@
package org.springframework.boot.web.embedded.undertow;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.Undertow.Builder;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import io.undertow.servlet.api.DeploymentManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xnio.channels.BoundChannel;
import org.springframework.boot.web.server.Compression;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
@ -56,33 +38,11 @@ import org.springframework.util.StringUtils;
* @since 2.0.0
* @see UndertowServletWebServerFactory
*/
public class UndertowServletWebServer implements WebServer {
private static final Log logger = LogFactory.getLog(UndertowServletWebServer.class);
private final Object monitor = new Object();
private final Builder builder;
private final DeploymentManager manager;
public class UndertowServletWebServer extends UndertowWebServer {
private final String contextPath;
private final boolean useForwardHeaders;
private final boolean autoStart;
private final Compression compression;
private final String serverHeader;
private final Duration shutdownGracePeriod;
private Undertow undertow;
private volatile boolean started = false;
private volatile GracefulShutdown gracefulShutdown;
private final DeploymentManager manager;
/**
* Create a new {@link UndertowServletWebServer} instance.
@ -91,7 +51,10 @@ public class UndertowServletWebServer implements WebServer {
* @param contextPath the root context path
* @param autoStart if the server should be started
* @param compression compression configuration
* @deprecated since 2.3.0 in favor of
* {@link #UndertowServletWebServer(io.undertow.Undertow.Builder, Iterable, String, boolean)}
*/
@Deprecated
public UndertowServletWebServer(Builder builder, DeploymentManager manager, String contextPath, boolean autoStart,
Compression compression) {
this(builder, manager, contextPath, false, autoStart, compression);
@ -105,7 +68,10 @@ public class UndertowServletWebServer implements WebServer {
* @param useForwardHeaders if x-forward headers should be used
* @param autoStart if the server should be started
* @param compression compression configuration
* @deprecated since 2.3.0 in favor of
* {@link #UndertowServletWebServer(io.undertow.Undertow.Builder, Iterable, String, boolean)}
*/
@Deprecated
public UndertowServletWebServer(Builder builder, DeploymentManager manager, String contextPath,
boolean useForwardHeaders, boolean autoStart, Compression compression) {
this(builder, manager, contextPath, useForwardHeaders, autoStart, compression, null);
@ -120,274 +86,60 @@ public class UndertowServletWebServer implements WebServer {
* @param autoStart if the server should be started
* @param compression compression configuration
* @param serverHeader string to be used in HTTP header
* @deprecated since 2.3.0 in favor of
* {@link #UndertowServletWebServer(io.undertow.Undertow.Builder, Iterable, String, boolean)}
*/
@Deprecated
public UndertowServletWebServer(Builder builder, DeploymentManager manager, String contextPath,
boolean useForwardHeaders, boolean autoStart, Compression compression, String serverHeader) {
this(builder, manager, contextPath, useForwardHeaders, autoStart, compression, serverHeader, null);
this(builder, UndertowWebServerFactoryDelegate.createHttpHandlerFactories(compression, useForwardHeaders,
serverHeader, null, new DeploymentManagerHttpHandlerFactory(manager)), contextPath, autoStart);
}
/**
* Create a new {@link UndertowServletWebServer} instance.
* @param builder the builder
* @param manager the deployment manager
* @param httpHandlerFactories the handler factories
* @param contextPath the root context path
* @param useForwardHeaders if x-forward headers should be used
* @param autoStart if the server should be started
* @param compression compression configuration
* @param serverHeader string to be used in HTTP header
* @param shutdownGracePeriod the period to wait for activity to cease when shutting
* down the server gracefully
* @since 2.3.0
*/
public UndertowServletWebServer(Builder builder, DeploymentManager manager, String contextPath,
boolean useForwardHeaders, boolean autoStart, Compression compression, String serverHeader,
Duration shutdownGracePeriod) {
this.builder = builder;
this.manager = manager;
public UndertowServletWebServer(Builder builder, Iterable<HttpHandlerFactory> httpHandlerFactories,
String contextPath, boolean autoStart) {
super(builder, httpHandlerFactories, autoStart);
this.contextPath = contextPath;
this.useForwardHeaders = useForwardHeaders;
this.autoStart = autoStart;
this.compression = compression;
this.serverHeader = serverHeader;
this.shutdownGracePeriod = shutdownGracePeriod;
}
@Override
public void start() throws WebServerException {
synchronized (this.monitor) {
if (this.started) {
return;
}
try {
if (!this.autoStart) {
return;
}
if (this.undertow == null) {
this.undertow = createUndertowServer();
}
this.undertow.start();
this.started = true;
UndertowServletWebServer.logger.info("Undertow started on port(s) " + getPortsDescription()
+ " with context path '" + this.contextPath + "'");
}
catch (Exception ex) {
try {
PortInUseException.ifPortBindingException(ex, (bindException) -> {
List<Port> failedPorts = getConfiguredPorts();
failedPorts.removeAll(getActualPorts());
if (failedPorts.size() == 1) {
throw new PortInUseException(failedPorts.get(0).getNumber());
}
});
throw new WebServerException("Unable to start embedded Undertow", ex);
}
finally {
stopSilently();
}
}
}
}
public DeploymentManager getDeploymentManager() {
synchronized (this.monitor) {
return this.manager;
}
}
private void stopSilently() {
try {
if (this.undertow != null) {
this.undertow.stop();
}
}
catch (Exception ex) {
// Ignore
}
this.manager = findManager(httpHandlerFactories);
}
private Undertow createUndertowServer() throws ServletException {
HttpHandler httpHandler = this.manager.start();
httpHandler = getContextHandler(httpHandler);
if (this.useForwardHeaders) {
httpHandler = Handlers.proxyPeerAddress(httpHandler);
}
if (StringUtils.hasText(this.serverHeader)) {
httpHandler = Handlers.header(httpHandler, "Server", this.serverHeader);
private DeploymentManager findManager(Iterable<HttpHandlerFactory> httpHandlerFactories) {
for (HttpHandlerFactory httpHandlerFactory : httpHandlerFactories) {
if (httpHandlerFactory instanceof DeploymentManagerHttpHandlerFactory) {
return ((DeploymentManagerHttpHandlerFactory) httpHandlerFactory).getDeploymentManager();
}
if (this.shutdownGracePeriod != null) {
GracefulShutdownHandler gracefulShutdownHandler = Handlers.gracefulShutdown(httpHandler);
this.gracefulShutdown = new UndertowGracefulShutdown(gracefulShutdownHandler, this.shutdownGracePeriod);
httpHandler = gracefulShutdownHandler;
}
else {
this.gracefulShutdown = GracefulShutdown.IMMEDIATE;
}
this.builder.setHandler(httpHandler);
return this.builder.build();
}
private HttpHandler getContextHandler(HttpHandler httpHandler) {
HttpHandler contextHandler = UndertowCompressionConfigurer.configureCompression(this.compression, httpHandler);
if (StringUtils.isEmpty(this.contextPath)) {
return contextHandler;
}
return Handlers.path().addPrefixPath(this.contextPath, contextHandler);
}
private String getPortsDescription() {
List<Port> ports = getActualPorts();
if (!ports.isEmpty()) {
return StringUtils.collectionToDelimitedString(ports, " ");
}
return "unknown";
}
private List<Port> getActualPorts() {
List<Port> ports = new ArrayList<>();
try {
if (!this.autoStart) {
ports.add(new Port(-1, "unknown"));
}
else {
for (BoundChannel channel : extractChannels()) {
ports.add(getPortFromChannel(channel));
}
}
}
catch (Exception ex) {
// Continue
}
return ports;
}
@SuppressWarnings("unchecked")
private List<BoundChannel> extractChannels() {
Field channelsField = ReflectionUtils.findField(Undertow.class, "channels");
ReflectionUtils.makeAccessible(channelsField);
return (List<BoundChannel>) ReflectionUtils.getField(channelsField, this.undertow);
}
private Port getPortFromChannel(BoundChannel channel) {
SocketAddress socketAddress = channel.getLocalAddress();
if (socketAddress instanceof InetSocketAddress) {
String protocol = (ReflectionUtils.findField(channel.getClass(), "ssl") != null) ? "https" : "http";
return new Port(((InetSocketAddress) socketAddress).getPort(), protocol);
}
return null;
}
private List<Port> getConfiguredPorts() {
List<Port> ports = new ArrayList<>();
for (Object listener : extractListeners()) {
try {
Port port = getPortFromListener(listener);
if (port.getNumber() != 0) {
ports.add(port);
}
}
catch (Exception ex) {
// Continue
}
}
return ports;
}
@SuppressWarnings("unchecked")
private List<Object> extractListeners() {
Field listenersField = ReflectionUtils.findField(Undertow.class, "listeners");
ReflectionUtils.makeAccessible(listenersField);
return (List<Object>) ReflectionUtils.getField(listenersField, this.undertow);
}
private Port getPortFromListener(Object listener) {
Field typeField = ReflectionUtils.findField(listener.getClass(), "type");
ReflectionUtils.makeAccessible(typeField);
String protocol = ReflectionUtils.getField(typeField, listener).toString();
Field portField = ReflectionUtils.findField(listener.getClass(), "port");
ReflectionUtils.makeAccessible(portField);
int port = (Integer) ReflectionUtils.getField(portField, listener);
return new Port(port, protocol);
}
@Override
public void stop() throws WebServerException {
synchronized (this.monitor) {
if (!this.started) {
return;
}
this.started = false;
try {
this.manager.stop();
this.manager.undeploy();
this.undertow.stop();
}
catch (Exception ex) {
throw new WebServerException("Unable to stop undertow", ex);
}
protected HttpHandler createHttpHandler() {
HttpHandler handler = super.createHttpHandler();
if (!StringUtils.isEmpty(this.contextPath)) {
handler = Handlers.path().addPrefixPath(this.contextPath, handler);
}
return handler;
}
@Override
public int getPort() {
List<Port> ports = getActualPorts();
if (ports.isEmpty()) {
return 0;
protected String getStartLogMessage() {
String message = super.getStartLogMessage();
if (StringUtils.hasText(this.contextPath)) {
message += " with context path '" + this.contextPath + "'";
}
return ports.get(0).getNumber();
}
@Override
public boolean shutDownGracefully() {
return this.gracefulShutdown.shutDownGracefully();
}
boolean inGracefulShutdown() {
return this.gracefulShutdown.isShuttingDown();
}
/**
* An active Undertow port.
*/
private static final class Port {
private final int number;
private final String protocol;
private Port(int number, String protocol) {
this.number = number;
this.protocol = protocol;
}
int getNumber() {
return this.number;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Port other = (Port) obj;
return this.number == other.number;
}
@Override
public int hashCode() {
return this.number;
}
@Override
public String toString() {
return this.number + " (" + this.protocol + ")";
return message;
}
public DeploymentManager getDeploymentManager() {
return this.manager;
}
}

@ -25,29 +25,19 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EventListener;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import io.undertow.Undertow;
import io.undertow.Undertow.Builder;
import io.undertow.UndertowOptions;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.accesslog.AccessLogHandler;
import io.undertow.server.handlers.accesslog.AccessLogReceiver;
import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver;
import io.undertow.server.handlers.resource.FileResourceManager;
import io.undertow.server.handlers.resource.Resource;
import io.undertow.server.handlers.resource.ResourceChangeListener;
@ -57,17 +47,12 @@ import io.undertow.server.session.SessionManager;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.ListenerInfo;
import io.undertow.servlet.api.MimeMapping;
import io.undertow.servlet.api.ServletContainerInitializerInfo;
import io.undertow.servlet.api.ServletStackTraces;
import io.undertow.servlet.core.DeploymentImpl;
import io.undertow.servlet.handlers.DefaultServlet;
import io.undertow.servlet.util.ImmediateInstanceFactory;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.Xnio;
import org.xnio.XnioWorker;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.MimeMappings.Mapping;
@ -100,34 +85,12 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
private static final Set<Class<?>> NO_CLASSES = Collections.emptySet();
private Set<UndertowBuilderCustomizer> builderCustomizers = new LinkedHashSet<>();
private UndertowWebServerFactoryDelegate delegate = new UndertowWebServerFactoryDelegate();
private Set<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers = new LinkedHashSet<>();
private ResourceLoader resourceLoader;
private Integer bufferSize;
private Integer ioThreads;
private Integer workerThreads;
private Boolean directBuffers;
private File accessLogDirectory;
private String accessLogPattern;
private String accessLogPrefix;
private String accessLogSuffix;
private boolean accessLogEnabled = false;
private boolean accessLogRotate = true;
private boolean useForwardHeaders;
private boolean eagerInitFilters = true;
/**
@ -158,14 +121,14 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
getJsp().setRegistered(false);
}
/**
* Set {@link UndertowBuilderCustomizer}s that should be applied to the Undertow
* {@link Builder}. Calling this method will replace any existing customizers.
* @param customizers the customizers to set
*/
@Override
public void setBuilderCustomizers(Collection<? extends UndertowBuilderCustomizer> customizers) {
Assert.notNull(customizers, "Customizers must not be null");
this.builderCustomizers = new LinkedHashSet<>(customizers);
this.delegate.setBuilderCustomizers(customizers);
}
@Override
public void addBuilderCustomizers(UndertowBuilderCustomizer... customizers) {
this.delegate.addBuilderCustomizers(customizers);
}
/**
@ -174,13 +137,74 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
* @return the customizers that will be applied
*/
public Collection<UndertowBuilderCustomizer> getBuilderCustomizers() {
return this.builderCustomizers;
return this.delegate.getBuilderCustomizers();
}
@Override
public void addBuilderCustomizers(UndertowBuilderCustomizer... customizers) {
Assert.notNull(customizers, "Customizers must not be null");
this.builderCustomizers.addAll(Arrays.asList(customizers));
public void setBufferSize(Integer bufferSize) {
this.delegate.setBufferSize(bufferSize);
}
@Override
public void setIoThreads(Integer ioThreads) {
this.delegate.setIoThreads(ioThreads);
}
@Override
public void setWorkerThreads(Integer workerThreads) {
this.delegate.setWorkerThreads(workerThreads);
}
@Override
public void setUseDirectBuffers(Boolean directBuffers) {
this.delegate.setUseDirectBuffers(directBuffers);
}
@Override
public void setAccessLogDirectory(File accessLogDirectory) {
this.delegate.setAccessLogDirectory(accessLogDirectory);
}
@Override
public void setAccessLogPattern(String accessLogPattern) {
this.delegate.setAccessLogPattern(accessLogPattern);
}
@Override
public void setAccessLogPrefix(String accessLogPrefix) {
this.delegate.setAccessLogPrefix(accessLogPrefix);
}
public String getAccessLogPrefix() {
return this.delegate.getAccessLogPrefix();
}
@Override
public void setAccessLogSuffix(String accessLogSuffix) {
this.delegate.setAccessLogSuffix(accessLogSuffix);
}
@Override
public void setAccessLogEnabled(boolean accessLogEnabled) {
this.delegate.setAccessLogEnabled(accessLogEnabled);
}
public boolean isAccessLogEnabled() {
return this.delegate.isAccessLogEnabled();
}
@Override
public void setAccessLogRotate(boolean accessLogRotate) {
this.delegate.setAccessLogRotate(accessLogRotate);
}
@Override
public void setUseForwardHeaders(boolean useForwardHeaders) {
this.delegate.setUseForwardHeaders(useForwardHeaders);
}
protected final boolean isUseForwardHeaders() {
return this.delegate.isUseForwardHeaders();
}
/**
@ -194,15 +218,6 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
this.deploymentInfoCustomizers = new LinkedHashSet<>(customizers);
}
/**
* Returns a mutable collection of the {@link UndertowDeploymentInfoCustomizer}s that
* will be applied to the Undertow {@link DeploymentInfo}.
* @return the customizers that will be applied
*/
public Collection<UndertowDeploymentInfoCustomizer> getDeploymentInfoCustomizers() {
return this.deploymentInfoCustomizers;
}
/**
* Add {@link UndertowDeploymentInfoCustomizer}s that should be used to customize the
* Undertow {@link DeploymentInfo}.
@ -213,56 +228,47 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
this.deploymentInfoCustomizers.addAll(Arrays.asList(customizers));
}
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
DeploymentManager manager = createDeploymentManager(initializers);
int port = getPort();
Builder builder = createBuilder(port);
return getUndertowWebServer(builder, manager, port);
/**
* Returns a mutable collection of the {@link UndertowDeploymentInfoCustomizer}s that
* will be applied to the Undertow {@link DeploymentInfo}.
* @return the customizers that will be applied
*/
public Collection<UndertowDeploymentInfoCustomizer> getDeploymentInfoCustomizers() {
return this.deploymentInfoCustomizers;
}
private Builder createBuilder(int port) {
Builder builder = Undertow.builder();
if (this.bufferSize != null) {
builder.setBufferSize(this.bufferSize);
}
if (this.ioThreads != null) {
builder.setIoThreads(this.ioThreads);
}
if (this.workerThreads != null) {
builder.setWorkerThreads(this.workerThreads);
}
if (this.directBuffers != null) {
builder.setDirectBuffers(this.directBuffers);
}
if (getSsl() != null && getSsl().isEnabled()) {
customizeSsl(builder);
}
else {
builder.addHttpListener(port, getListenAddress());
}
builder.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 0);
for (UndertowBuilderCustomizer customizer : this.builderCustomizers) {
customizer.customize(builder);
}
return builder;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
private void customizeSsl(Builder builder) {
new SslBuilderCustomizer(getPort(), getAddress(), getSsl(), getSslStoreProvider()).customize(builder);
if (getHttp2() != null) {
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, getHttp2().isEnabled());
}
/**
* Return if filters should be initialized eagerly.
* @return {@code true} if filters are initialized eagerly, otherwise {@code false}.
* @since 2.0.0
*/
public boolean isEagerInitFilters() {
return this.eagerInitFilters;
}
private String getListenAddress() {
if (getAddress() == null) {
return "0.0.0.0";
/**
* Set whether filters should be initialized eagerly.
* @param eagerInitFilters {@code true} if filters are initialized eagerly, otherwise
* {@code false}.
* @since 2.0.0
*/
public void setEagerInitFilters(boolean eagerInitFilters) {
this.eagerInitFilters = eagerInitFilters;
}
return getAddress().getHostAddress();
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
Builder builder = this.delegate.createBuilder(this);
DeploymentManager manager = createManager(initializers);
return getUndertowWebServer(builder, manager, getPort());
}
private DeploymentManager createDeploymentManager(ServletContextInitializer... initializers) {
private DeploymentManager createManager(ServletContextInitializer... initializers) {
DeploymentInfo deployment = Servlets.deployment();
registerServletContainerInitializerToDriveServletContextInitializers(deployment, initializers);
deployment.setClassLoader(getServletClassLoader());
@ -281,9 +287,6 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
for (UndertowDeploymentInfoCustomizer customizer : this.deploymentInfoCustomizers) {
customizer.customize(deployment);
}
if (isAccessLogEnabled()) {
configureAccessLog(deployment);
}
if (getSession().isPersistent()) {
File dir = getValidSessionStoreDir();
deployment.setSessionPersistenceManager(new FileSessionPersistence(dir));
@ -305,42 +308,6 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
return timeoutDuration == null || timeoutDuration.isZero() || timeoutDuration.isNegative();
}
private void configureAccessLog(DeploymentInfo deploymentInfo) {
try {
createAccessLogDirectoryIfNecessary();
XnioWorker worker = createWorker();
String prefix = (this.accessLogPrefix != null) ? this.accessLogPrefix : "access_log.";
DefaultAccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver(worker, this.accessLogDirectory,
prefix, this.accessLogSuffix, this.accessLogRotate);
EventListener listener = new AccessLogShutdownListener(worker, accessLogReceiver);
deploymentInfo.addListener(
new ListenerInfo(AccessLogShutdownListener.class, new ImmediateInstanceFactory<>(listener)));
deploymentInfo
.addInitialHandlerChainWrapper((handler) -> createAccessLogHandler(handler, accessLogReceiver));
}
catch (IOException ex) {
throw new IllegalStateException("Failed to create AccessLogHandler", ex);
}
}
private AccessLogHandler createAccessLogHandler(HttpHandler handler, AccessLogReceiver accessLogReceiver) {
createAccessLogDirectoryIfNecessary();
String formatString = (this.accessLogPattern != null) ? this.accessLogPattern : "common";
return new AccessLogHandler(handler, accessLogReceiver, formatString, Undertow.class.getClassLoader());
}
private void createAccessLogDirectoryIfNecessary() {
Assert.state(this.accessLogDirectory != null, "Access log directory is not set");
if (!this.accessLogDirectory.isDirectory() && !this.accessLogDirectory.mkdirs()) {
throw new IllegalStateException("Failed to create access log directory '" + this.accessLogDirectory + "'");
}
}
private XnioWorker createWorker() throws IOException {
Xnio xnio = Xnio.getInstance(Undertow.class.getClassLoader());
return xnio.createWorker(OptionMap.builder().set(Options.THREAD_DAEMON, true).getMap());
}
private void addLocaleMappings(DeploymentInfo deployment) {
getLocaleCharsetMappings().forEach(
(locale, charset) -> deployment.addLocaleCharsetMapping(locale.toString(), charset.toString()));
@ -449,99 +416,30 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
* @return a new {@link UndertowServletWebServer} instance
*/
protected UndertowServletWebServer getUndertowWebServer(Builder builder, DeploymentManager manager, int port) {
return new UndertowServletWebServer(builder, manager, getContextPath(), isUseForwardHeaders(), port >= 0,
getCompression(), getServerHeader(), getShutdown().getGracePeriod());
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void setBufferSize(Integer bufferSize) {
this.bufferSize = bufferSize;
}
@Override
public void setIoThreads(Integer ioThreads) {
this.ioThreads = ioThreads;
}
@Override
public void setWorkerThreads(Integer workerThreads) {
this.workerThreads = workerThreads;
}
@Override
public void setUseDirectBuffers(Boolean directBuffers) {
this.directBuffers = directBuffers;
}
@Override
public void setAccessLogDirectory(File accessLogDirectory) {
this.accessLogDirectory = accessLogDirectory;
}
@Override
public void setAccessLogPattern(String accessLogPattern) {
this.accessLogPattern = accessLogPattern;
}
public String getAccessLogPrefix() {
return this.accessLogPrefix;
}
@Override
public void setAccessLogPrefix(String accessLogPrefix) {
this.accessLogPrefix = accessLogPrefix;
List<HttpHandlerFactory> httpHandlerFactories = this.delegate.createHttpHandlerFactories(this,
new DeploymentManagerHttpHandlerFactory(manager));
return new UndertowServletWebServer(builder, httpHandlerFactories, getContextPath(), port >= 0);
}
@Override
public void setAccessLogSuffix(String accessLogSuffix) {
this.accessLogSuffix = accessLogSuffix;
}
@Override
public void setAccessLogEnabled(boolean accessLogEnabled) {
this.accessLogEnabled = accessLogEnabled;
}
public boolean isAccessLogEnabled() {
return this.accessLogEnabled;
}
/**
* {@link ServletContainerInitializer} to initialize {@link ServletContextInitializer
* ServletContextInitializers}.
*/
private static class Initializer implements ServletContainerInitializer {
@Override
public void setAccessLogRotate(boolean accessLogRotate) {
this.accessLogRotate = accessLogRotate;
}
private final ServletContextInitializer[] initializers;
protected final boolean isUseForwardHeaders() {
return this.useForwardHeaders;
Initializer(ServletContextInitializer[] initializers) {
this.initializers = initializers;
}
@Override
public void setUseForwardHeaders(boolean useForwardHeaders) {
this.useForwardHeaders = useForwardHeaders;
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
for (ServletContextInitializer initializer : this.initializers) {
initializer.onStartup(servletContext);
}
/**
* Return if filters should be initialized eagerly.
* @return {@code true} if filters are initialized eagerly, otherwise {@code false}.
* @since 2.0.0
*/
public boolean isEagerInitFilters() {
return this.eagerInitFilters;
}
/**
* Set whether filters should be initialized eagerly.
* @param eagerInitFilters {@code true} if filters are initialized eagerly, otherwise
* {@code false}.
* @since 2.0.0
*/
public void setEagerInitFilters(boolean eagerInitFilters) {
this.eagerInitFilters = eagerInitFilters;
}
/**
@ -603,26 +501,8 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
}
/**
* {@link ServletContainerInitializer} to initialize {@link ServletContextInitializer
* ServletContextInitializers}.
* {@link ResourceManager} to hide Spring Boot loader classes.
*/
private static class Initializer implements ServletContainerInitializer {
private final ServletContextInitializer[] initializers;
Initializer(ServletContextInitializer[] initializers) {
this.initializers = initializers;
}
@Override
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
for (ServletContextInitializer initializer : this.initializers) {
initializer.onStartup(servletContext);
}
}
}
private static final class LoaderHidingResourceManager implements ResourceManager {
private final ResourceManager delegate;
@ -661,36 +541,4 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
}
private static class AccessLogShutdownListener implements ServletContextListener {
private final XnioWorker worker;
private final DefaultAccessLogReceiver accessLogReceiver;
AccessLogShutdownListener(XnioWorker worker, DefaultAccessLogReceiver accessLogReceiver) {
this.worker = worker;
this.accessLogReceiver = accessLogReceiver;
}
@Override
public void contextInitialized(ServletContextEvent sce) {
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
try {
this.accessLogReceiver.close();
this.worker.shutdown();
this.worker.awaitTermination(30, TimeUnit.SECONDS);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}

@ -17,13 +17,17 @@
package org.springframework.boot.web.embedded.undertow;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xnio.channels.BoundChannel;
@ -32,6 +36,7 @@ import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
@ -55,16 +60,18 @@ public class UndertowWebServer implements WebServer {
private final Undertow.Builder builder;
private final boolean autoStart;
private final Closeable closeable;
private final Iterable<HttpHandlerFactory> httpHandlerFactories;
private final GracefulShutdown gracefulShutdown;
private final boolean autoStart;
private Undertow undertow;
private volatile boolean started = false;
private volatile GracefulShutdown gracefulShutdown;
private volatile List<Closeable> closeables;
/**
* Create a new {@link UndertowWebServer} instance.
* @param builder the builder
@ -80,25 +87,26 @@ public class UndertowWebServer implements WebServer {
* @param autoStart if the server should be started
* @param closeable called when the server is stopped
* @since 2.0.4
* @deprecated since 2.3.0 in favor of
* {@link #UndertowWebServer(io.undertow.Undertow.Builder, Iterable, boolean)}
*/
@Deprecated
public UndertowWebServer(Undertow.Builder builder, boolean autoStart, Closeable closeable) {
this(builder, autoStart, closeable, GracefulShutdown.IMMEDIATE);
this(builder, Collections.singleton(new CloseableHttpHandlerFactory(closeable)), autoStart);
}
/**
* Create a new {@link UndertowWebServer} instance.
* @param builder the builder
* @param httpHandlerFactories the handler factories
* @param autoStart if the server should be started
* @param closeable called when the server is stopped
* @param gracefulShutdown handler for graceful shutdown
* @since 2.3.0
*/
public UndertowWebServer(Undertow.Builder builder, boolean autoStart, Closeable closeable,
GracefulShutdown gracefulShutdown) {
public UndertowWebServer(Undertow.Builder builder, Iterable<HttpHandlerFactory> httpHandlerFactories,
boolean autoStart) {
this.builder = builder;
this.httpHandlerFactories = httpHandlerFactories;
this.autoStart = autoStart;
this.closeable = closeable;
this.gracefulShutdown = gracefulShutdown;
}
@Override
@ -112,11 +120,12 @@ public class UndertowWebServer implements WebServer {
return;
}
if (this.undertow == null) {
this.undertow = this.builder.build();
this.undertow = createUndertowServer();
}
this.undertow.start();
this.started = true;
logger.info("Undertow started on port(s) " + getPortsDescription());
String message = getStartLogMessage();
logger.info(message);
}
catch (Exception ex) {
try {
@ -140,7 +149,7 @@ public class UndertowWebServer implements WebServer {
try {
if (this.undertow != null) {
this.undertow.stop();
this.closeable.close();
this.closeables.forEach(this::closeSilently);
}
}
catch (Exception ex) {
@ -148,6 +157,37 @@ public class UndertowWebServer implements WebServer {
}
}
private void closeSilently(Closeable closeable) {
try {
closeable.close();
}
catch (Exception ex) {
}
}
private Undertow createUndertowServer() {
this.closeables = new ArrayList<>();
this.gracefulShutdown = null;
HttpHandler handler = createHttpHandler();
this.builder.setHandler(handler);
return this.builder.build();
}
protected HttpHandler createHttpHandler() {
HttpHandler handler = null;
for (HttpHandlerFactory factory : this.httpHandlerFactories) {
handler = factory.getHandler(handler);
if (handler instanceof Closeable) {
this.closeables.add((Closeable) handler);
}
if (handler instanceof GracefulShutdown) {
Assert.isNull(this.gracefulShutdown, "Only a single GracefulShutdown handler can be defined");
this.gracefulShutdown = (GracefulShutdown) handler;
}
}
return handler;
}
private String getPortsDescription() {
List<UndertowWebServer.Port> ports = getActualPorts();
if (!ports.isEmpty()) {
@ -156,11 +196,11 @@ public class UndertowWebServer implements WebServer {
return "unknown";
}
private List<UndertowWebServer.Port> getActualPorts() {
List<UndertowWebServer.Port> ports = new ArrayList<>();
private List<Port> getActualPorts() {
List<Port> ports = new ArrayList<>();
try {
if (!this.autoStart) {
ports.add(new UndertowWebServer.Port(-1, "unknown"));
ports.add(new Port(-1, "unknown"));
}
else {
for (BoundChannel channel : extractChannels()) {
@ -192,10 +232,13 @@ public class UndertowWebServer implements WebServer {
}
private List<UndertowWebServer.Port> getConfiguredPorts() {
List<UndertowWebServer.Port> ports = new ArrayList<>();
List<Port> ports = new ArrayList<>();
for (Object listener : extractListeners()) {
try {
ports.add(getPortFromListener(listener));
Port port = getPortFromListener(listener);
if (port.getNumber() != 0) {
ports.add(port);
}
}
catch (Exception ex) {
// Continue
@ -230,8 +273,8 @@ public class UndertowWebServer implements WebServer {
this.started = false;
try {
this.undertow.stop();
if (this.closeable != null) {
this.closeable.close();
for (Closeable closeable : this.closeables) {
closeable.close();
}
}
catch (Exception ex) {
@ -242,7 +285,7 @@ public class UndertowWebServer implements WebServer {
@Override
public int getPort() {
List<UndertowWebServer.Port> ports = getActualPorts();
List<Port> ports = getActualPorts();
if (ports.isEmpty()) {
return 0;
}
@ -251,11 +294,15 @@ public class UndertowWebServer implements WebServer {
@Override
public boolean shutDownGracefully() {
return (this.gracefulShutdown != null) && this.gracefulShutdown.shutDownGracefully();
return (this.gracefulShutdown != null) ? this.gracefulShutdown.shutDownGracefully() : false;
}
boolean inGracefulShutdown() {
return (this.gracefulShutdown != null) && this.gracefulShutdown.isShuttingDown();
return (this.gracefulShutdown != null) ? this.gracefulShutdown.isShuttingDown() : false;
}
protected String getStartLogMessage() {
return "Undertow started on port(s) " + getPortsDescription();
}
/**
@ -303,4 +350,44 @@ public class UndertowWebServer implements WebServer {
}
/**
* {@link HttpHandlerFactory} to wrap a closable.
*/
private static final class CloseableHttpHandlerFactory implements HttpHandlerFactory {
private final Closeable closeable;
private CloseableHttpHandlerFactory(Closeable closeable) {
this.closeable = closeable;
}
@Override
public HttpHandler getHandler(HttpHandler next) {
if (this.closeable == null) {
return next;
}
return new CloseableHttpHandler() {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
next.handleRequest(exchange);
}
@Override
public void close() throws IOException {
CloseableHttpHandlerFactory.this.closeable.close();
}
};
}
}
/**
* {@link Closeable} {@link HttpHandler}.
*/
private interface CloseableHttpHandler extends HttpHandler, Closeable {
}
}

@ -0,0 +1,209 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.embedded.undertow;
import java.io.File;
import java.net.InetAddress;
import java.time.Duration;
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 io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.Undertow.Builder;
import io.undertow.UndertowOptions;
import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory;
import org.springframework.boot.web.server.Compression;
import org.springframework.boot.web.server.Http2;
import org.springframework.boot.web.server.Ssl;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Delegate class used by {@link UndertowServletWebServerFactory} and
* {@link UndertowReactiveWebServerFactory}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class UndertowWebServerFactoryDelegate {
private Set<UndertowBuilderCustomizer> builderCustomizers = new LinkedHashSet<>();
private Integer bufferSize;
private Integer ioThreads;
private Integer workerThreads;
private Boolean directBuffers;
private File accessLogDirectory;
private String accessLogPattern;
private String accessLogPrefix;
private String accessLogSuffix;
private boolean accessLogEnabled = false;
private boolean accessLogRotate = true;
private boolean useForwardHeaders;
void setBuilderCustomizers(Collection<? extends UndertowBuilderCustomizer> customizers) {
Assert.notNull(customizers, "Customizers must not be null");
this.builderCustomizers = new LinkedHashSet<>(customizers);
}
void addBuilderCustomizers(UndertowBuilderCustomizer... customizers) {
Assert.notNull(customizers, "Customizers must not be null");
this.builderCustomizers.addAll(Arrays.asList(customizers));
}
Collection<UndertowBuilderCustomizer> getBuilderCustomizers() {
return this.builderCustomizers;
}
void setBufferSize(Integer bufferSize) {
this.bufferSize = bufferSize;
}
void setIoThreads(Integer ioThreads) {
this.ioThreads = ioThreads;
}
void setWorkerThreads(Integer workerThreads) {
this.workerThreads = workerThreads;
}
void setUseDirectBuffers(Boolean directBuffers) {
this.directBuffers = directBuffers;
}
void setAccessLogDirectory(File accessLogDirectory) {
this.accessLogDirectory = accessLogDirectory;
}
void setAccessLogPattern(String accessLogPattern) {
this.accessLogPattern = accessLogPattern;
}
void setAccessLogPrefix(String accessLogPrefix) {
this.accessLogPrefix = accessLogPrefix;
}
String getAccessLogPrefix() {
return this.accessLogPrefix;
}
void setAccessLogSuffix(String accessLogSuffix) {
this.accessLogSuffix = accessLogSuffix;
}
void setAccessLogEnabled(boolean accessLogEnabled) {
this.accessLogEnabled = accessLogEnabled;
}
boolean isAccessLogEnabled() {
return this.accessLogEnabled;
}
void setAccessLogRotate(boolean accessLogRotate) {
this.accessLogRotate = accessLogRotate;
}
void setUseForwardHeaders(boolean useForwardHeaders) {
this.useForwardHeaders = useForwardHeaders;
}
boolean isUseForwardHeaders() {
return this.useForwardHeaders;
}
Builder createBuilder(AbstractConfigurableWebServerFactory factory) {
Ssl ssl = factory.getSsl();
InetAddress address = factory.getAddress();
int port = factory.getPort();
Builder builder = Undertow.builder();
if (this.bufferSize != null) {
builder.setBufferSize(this.bufferSize);
}
if (this.ioThreads != null) {
builder.setIoThreads(this.ioThreads);
}
if (this.workerThreads != null) {
builder.setWorkerThreads(this.workerThreads);
}
if (this.directBuffers != null) {
builder.setDirectBuffers(this.directBuffers);
}
if (ssl != null && ssl.isEnabled()) {
new SslBuilderCustomizer(factory.getPort(), address, ssl, factory.getSslStoreProvider()).customize(builder);
Http2 http2 = factory.getHttp2();
if (http2 != null) {
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, http2.isEnabled());
}
}
else {
builder.addHttpListener(port, (address != null) ? address.getHostAddress() : "0.0.0.0");
}
builder.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 0);
for (UndertowBuilderCustomizer customizer : this.builderCustomizers) {
customizer.customize(builder);
}
return builder;
}
List<HttpHandlerFactory> createHttpHandlerFactories(AbstractConfigurableWebServerFactory webServerFactory,
HttpHandlerFactory... initialHttpHandlerFactories) {
List<HttpHandlerFactory> factories = createHttpHandlerFactories(webServerFactory.getCompression(),
this.useForwardHeaders, webServerFactory.getServerHeader(),
webServerFactory.getShutdown().getGracePeriod(), initialHttpHandlerFactories);
if (isAccessLogEnabled()) {
factories.add(new AccessLogHttpHandlerFactory(this.accessLogDirectory, this.accessLogPattern,
this.accessLogPrefix, this.accessLogSuffix, this.accessLogRotate));
}
return factories;
}
static List<HttpHandlerFactory> createHttpHandlerFactories(Compression compression, boolean useForwardHeaders,
String serverHeader, Duration shutdownGracePeriod, HttpHandlerFactory... initialHttpHandlerFactories) {
List<HttpHandlerFactory> factories = new ArrayList<HttpHandlerFactory>();
factories.addAll(Arrays.asList(initialHttpHandlerFactories));
if (compression != null && compression.getEnabled()) {
factories.add(new CompressionHttpHandlerFactory(compression));
}
if (useForwardHeaders) {
factories.add(Handlers::proxyPeerAddress);
}
if (StringUtils.hasText(serverHeader)) {
factories.add((next) -> Handlers.header(next, "Server", serverHeader));
}
if (shutdownGracePeriod != null) {
factories.add((next) -> new GracefulShutdownHttpHandler(next, shutdownGracePeriod));
}
return factories;
}
}
Loading…
Cancel
Save