diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/AccessLogHttpHandlerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/AccessLogHttpHandlerFactory.java new file mode 100644 index 000000000000..03dbf60f13cb --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/AccessLogHttpHandlerFactory.java @@ -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(); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowCompressionConfigurer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/CompressionHttpHandlerFactory.java similarity index 83% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowCompressionConfigurer.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/CompressionHttpHandlerFactory.java index 02cd061799ed..610c9d8dfbcf 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowCompressionConfigurer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/CompressionHttpHandlerFactory.java @@ -37,30 +37,28 @@ 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 @@ private static Predicate[] getCompressionPredicates(Compression compression) { return predicates.toArray(new Predicate[0]); } + /** + * Predicate used to match specific mime types. + */ private static class CompressibleMimeTypePredicate implements Predicate { private final List mimeTypes; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/ConfigurableUndertowWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/ConfigurableUndertowWebServerFactory.java index 0dd8e30ad54d..2861cafe9c2c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/ConfigurableUndertowWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/ConfigurableUndertowWebServerFactory.java @@ -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 @@ */ 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 customizers); + /** * Add {@link UndertowBuilderCustomizer}s that should be used to customize the * Undertow {@link Builder}. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/DeploymentManagerHttpHandlerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/DeploymentManagerHttpHandlerFactory.java new file mode 100644 index 000000000000..b9312c7bca1c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/DeploymentManagerHttpHandlerFactory.java @@ -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; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowGracefulShutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/GracefulShutdownHttpHandler.java similarity index 72% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowGracefulShutdown.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/GracefulShutdownHttpHandler.java index dc2753a61c2d..4774b15898fc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowGracefulShutdown.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/GracefulShutdownHttpHandler.java @@ -18,6 +18,7 @@ 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.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(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/HttpHandlerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/HttpHandlerFactory.java new file mode 100644 index 000000000000..f05a1ec93069 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/HttpHandlerFactory.java @@ -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: + * + * + * @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); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java index 27348dd0b159..371bb8c1f334 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java @@ -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 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerFactory implements ConfigurableUndertowWebServerFactory { - private Set 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 UndertowReactiveWebServerFactory(int port) { } @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); - } - - 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; - } - - 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); - } - } - - 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 customizeSsl(Undertow.Builder builder) { - new SslBuilderCustomizer(getPort(), getAddress(), getSsl(), getSslStoreProvider()).customize(builder); - if (getHttp2() != null) { - builder.setServerOption(UndertowOptions.ENABLE_HTTP2, getHttp2().isEnabled()); - } - } - - private String getListenAddress() { - if (getAddress() == null) { - return "0.0.0.0"; - } - return getAddress().getHostAddress(); + public void setBuilderCustomizers(Collection customizers) { + this.delegate.setBuilderCustomizers(customizers); } @Override - public void setAccessLogDirectory(File accessLogDirectory) { - this.accessLogDirectory = accessLogDirectory; + public void addBuilderCustomizers(UndertowBuilderCustomizer... customizers) { + this.delegate.addBuilderCustomizers(customizers); } - @Override - public void setAccessLogPattern(String accessLogPattern) { - this.accessLogPattern = accessLogPattern; + /** + * 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 getBuilderCustomizers() { + return this.delegate.getBuilderCustomizers(); } @Override - public void setAccessLogPrefix(String accessLogPrefix) { - this.accessLogPrefix = accessLogPrefix; + public void setBufferSize(Integer bufferSize) { + this.delegate.setBufferSize(bufferSize); } @Override - public void setAccessLogSuffix(String accessLogSuffix) { - this.accessLogSuffix = accessLogSuffix; + public void setIoThreads(Integer ioThreads) { + this.delegate.setIoThreads(ioThreads); } - public boolean isAccessLogEnabled() { - return this.accessLogEnabled; + @Override + public void setWorkerThreads(Integer workerThreads) { + this.delegate.setWorkerThreads(workerThreads); } @Override - public void setAccessLogEnabled(boolean accessLogEnabled) { - this.accessLogEnabled = accessLogEnabled; + public void setUseDirectBuffers(Boolean directBuffers) { + this.delegate.setUseDirectBuffers(directBuffers); } @Override - public void setAccessLogRotate(boolean accessLogRotate) { - this.accessLogRotate = accessLogRotate; + public void setUseForwardHeaders(boolean useForwardHeaders) { + this.delegate.setUseForwardHeaders(useForwardHeaders); } protected final boolean isUseForwardHeaders() { - return this.useForwardHeaders; - } - - @Override - public void setUseForwardHeaders(boolean useForwardHeaders) { - this.useForwardHeaders = useForwardHeaders; + return this.delegate.isUseForwardHeaders(); } @Override - public void setBufferSize(Integer bufferSize) { - this.bufferSize = bufferSize; + public void setAccessLogDirectory(File accessLogDirectory) { + this.delegate.setAccessLogDirectory(accessLogDirectory); } @Override - public void setIoThreads(Integer ioThreads) { - this.ioThreads = ioThreads; + public void setAccessLogPattern(String accessLogPattern) { + this.delegate.setAccessLogPattern(accessLogPattern); } @Override - public void setWorkerThreads(Integer workerThreads) { - this.workerThreads = workerThreads; + public void setAccessLogPrefix(String accessLogPrefix) { + this.delegate.setAccessLogPrefix(accessLogPrefix); } @Override - public void setUseDirectBuffers(Boolean directBuffers) { - this.directBuffers = directBuffers; + public void setAccessLogSuffix(String accessLogSuffix) { + this.delegate.setAccessLogSuffix(accessLogSuffix); } - /** - * 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 customizers) { - Assert.notNull(customizers, "Customizers must not be null"); - this.builderCustomizers = new LinkedHashSet<>(customizers); + public boolean isAccessLogEnabled() { + return this.delegate.isAccessLogEnabled(); } - /** - * 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 getBuilderCustomizers() { - return this.builderCustomizers; + @Override + public void setAccessLogEnabled(boolean accessLogEnabled) { + this.delegate.setAccessLogEnabled(accessLogEnabled); } - /** - * 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)); + public void setAccessLogRotate(boolean accessLogRotate) { + this.delegate.setAccessLogRotate(accessLogRotate); } - 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; - } - + @Override + public WebServer getWebServer(org.springframework.http.server.reactive.HttpHandler httpHandler) { + Undertow.Builder builder = this.delegate.createBuilder(this); + List httpHandlerFactories = this.delegate.createHttpHandlerFactories(this, + (next) -> new UndertowHttpHandlerAdapter(httpHandler)); + return new UndertowWebServer(builder, httpHandlerFactories, getPort() >= 0); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java index 04d8cd2abb50..1a908235632f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java @@ -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 @@ * @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 UndertowServletWebServer(Builder builder, DeploymentManager manager, Stri * @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 UndertowServletWebServer(Builder builder, DeploymentManager manager, Stri * @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 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 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; - } + this.manager = findManager(httpHandlerFactories); } - private void stopSilently() { - try { - if (this.undertow != null) { - this.undertow.stop(); + private DeploymentManager findManager(Iterable httpHandlerFactories) { + for (HttpHandlerFactory httpHandlerFactory : httpHandlerFactories) { + if (httpHandlerFactory instanceof DeploymentManagerHttpHandlerFactory) { + return ((DeploymentManagerHttpHandlerFactory) httpHandlerFactory).getDeploymentManager(); } } - catch (Exception ex) { - // Ignore - } - } - - 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); - } - 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 ports = getActualPorts(); - if (!ports.isEmpty()) { - return StringUtils.collectionToDelimitedString(ports, " "); - } - return "unknown"; - } - - private List getActualPorts() { - List 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 extractChannels() { - Field channelsField = ReflectionUtils.findField(Undertow.class, "channels"); - ReflectionUtils.makeAccessible(channelsField); - return (List) 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 getConfiguredPorts() { - List 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 extractListeners() { - Field listenersField = ReflectionUtils.findField(Undertow.class, "listeners"); - ReflectionUtils.makeAccessible(listenersField); - return (List) 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 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(); + return message; } - @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 + ")"; - } - + public DeploymentManager getDeploymentManager() { + return this.manager; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java index dc5813bb0491..fd7be8adb2e3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java @@ -25,29 +25,19 @@ 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.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> NO_CLASSES = Collections.emptySet(); - private Set builderCustomizers = new LinkedHashSet<>(); + private UndertowWebServerFactoryDelegate delegate = new UndertowWebServerFactoryDelegate(); private Set 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 UndertowServletWebServerFactory(String contextPath, int port) { 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 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 void setBuilderCustomizers(Collection 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 void setDeploymentInfoCustomizers(Collection(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 getDeploymentInfoCustomizers() { - return this.deploymentInfoCustomizers; - } - /** * Add {@link UndertowDeploymentInfoCustomizer}s that should be used to customize the * Undertow {@link DeploymentInfo}. @@ -213,56 +228,47 @@ public void addDeploymentInfoCustomizers(UndertowDeploymentInfoCustomizer... cus this.deploymentInfoCustomizers.addAll(Arrays.asList(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 getDeploymentInfoCustomizers() { + return this.deploymentInfoCustomizers; + } + @Override - public WebServer getWebServer(ServletContextInitializer... initializers) { - DeploymentManager manager = createDeploymentManager(initializers); - int port = getPort(); - Builder builder = createBuilder(port); - return getUndertowWebServer(builder, manager, port); + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; } - 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; + /** + * 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 void customizeSsl(Builder builder) { - new SslBuilderCustomizer(getPort(), getAddress(), getSsl(), getSslStoreProvider()).customize(builder); - if (getHttp2() != null) { - builder.setServerOption(UndertowOptions.ENABLE_HTTP2, getHttp2().isEnabled()); - } + /** + * 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; } - private String getListenAddress() { - if (getAddress() == null) { - return "0.0.0.0"; - } - 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 @@ private DeploymentManager createDeploymentManager(ServletContextInitializer... i 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 @@ private boolean isZeroOrLess(Duration timeoutDuration) { 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 @@ private void removeSuperfluousMimeMappings(DeploymentImpl deployment, Deployment * @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; - } - - @Override - public void setAccessLogSuffix(String accessLogSuffix) { - this.accessLogSuffix = accessLogSuffix; - } - - @Override - public void setAccessLogEnabled(boolean accessLogEnabled) { - this.accessLogEnabled = accessLogEnabled; - } - - public boolean isAccessLogEnabled() { - return this.accessLogEnabled; + List httpHandlerFactories = this.delegate.createHttpHandlerFactories(this, + new DeploymentManagerHttpHandlerFactory(manager)); + return new UndertowServletWebServer(builder, httpHandlerFactories, getContextPath(), port >= 0); } - @Override - public void setAccessLogRotate(boolean accessLogRotate) { - this.accessLogRotate = accessLogRotate; - } + /** + * {@link ServletContainerInitializer} to initialize {@link ServletContextInitializer + * ServletContextInitializers}. + */ + private static class Initializer implements ServletContainerInitializer { - protected final boolean isUseForwardHeaders() { - return this.useForwardHeaders; - } + private final ServletContextInitializer[] initializers; - @Override - public void setUseForwardHeaders(boolean useForwardHeaders) { - this.useForwardHeaders = useForwardHeaders; - } + Initializer(ServletContextInitializer[] initializers) { + this.initializers = initializers; + } - /** - * 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; - } + @Override + public void onStartup(Set> classes, ServletContext servletContext) throws ServletException { + for (ServletContextInitializer initializer : this.initializers) { + initializer.onStartup(servletContext); + } + } - /** - * 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 @@ private URLResource getMetaInfResource(URL resourceJar, String path) { } /** - * {@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> 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 void close() throws IOException { } - 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(); - } - } - - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java index b023c3af92af..41aa84098f61 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java @@ -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.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 httpHandlerFactories; - private final GracefulShutdown gracefulShutdown; + private final boolean autoStart; private Undertow undertow; private volatile boolean started = false; + private volatile GracefulShutdown gracefulShutdown; + + private volatile List closeables; + /** * Create a new {@link UndertowWebServer} instance. * @param builder the builder @@ -80,25 +87,26 @@ public UndertowWebServer(Undertow.Builder builder, boolean autoStart) { * @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 httpHandlerFactories, + boolean autoStart) { this.builder = builder; + this.httpHandlerFactories = httpHandlerFactories; this.autoStart = autoStart; - this.closeable = closeable; - this.gracefulShutdown = gracefulShutdown; } @Override @@ -112,11 +120,12 @@ public void start() throws WebServerException { 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 @@ private void stopSilently() { try { if (this.undertow != null) { this.undertow.stop(); - this.closeable.close(); + this.closeables.forEach(this::closeSilently); } } catch (Exception ex) { @@ -148,6 +157,37 @@ private void stopSilently() { } } + 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 ports = getActualPorts(); if (!ports.isEmpty()) { @@ -156,11 +196,11 @@ private String getPortsDescription() { return "unknown"; } - private List getActualPorts() { - List ports = new ArrayList<>(); + private List getActualPorts() { + List 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 @@ private UndertowWebServer.Port getPortFromChannel(BoundChannel channel) { } private List getConfiguredPorts() { - List ports = new ArrayList<>(); + List 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 void stop() throws WebServerException { 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 void stop() throws WebServerException { @Override public int getPort() { - List ports = getActualPorts(); + List ports = getActualPorts(); if (ports.isEmpty()) { return 0; } @@ -251,11 +294,15 @@ public int getPort() { @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 String toString() { } + /** + * {@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 { + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java new file mode 100644 index 000000000000..d25aec95bee8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java @@ -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 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 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 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 createHttpHandlerFactories(AbstractConfigurableWebServerFactory webServerFactory, + HttpHandlerFactory... initialHttpHandlerFactories) { + List 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 createHttpHandlerFactories(Compression compression, boolean useForwardHeaders, + String serverHeader, Duration shutdownGracePeriod, HttpHandlerFactory... initialHttpHandlerFactories) { + List factories = new ArrayList(); + 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; + } + +}