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 7fa41b62e80..80f0763d841 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,6 +16,7 @@ package org.springframework.boot.web.embedded.undertow; +import java.io.Closeable; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -28,7 +29,6 @@ import io.undertow.Undertow; 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.servlet.api.DeploymentInfo; import org.xnio.OptionMap; @@ -96,9 +96,8 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF public WebServer getWebServer( org.springframework.http.server.reactive.HttpHandler httpHandler) { Undertow.Builder builder = createBuilder(getPort()); - HttpHandler handler = createUndertowHandler(httpHandler); - builder.setHandler(handler); - return new UndertowWebServer(builder, getPort() >= 0); + Closeable closeable = configureHandler(builder, httpHandler); + return new UndertowWebServer(builder, getPort() >= 0, closeable); } private Undertow.Builder createBuilder(int port) { @@ -127,7 +126,7 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF return builder; } - private HttpHandler createUndertowHandler( + private Closeable configureHandler(Undertow.Builder builder, org.springframework.http.server.reactive.HttpHandler httpHandler) { HttpHandler handler = new UndertowHttpHandlerAdapter(httpHandler); if (this.useForwardHeaders) { @@ -135,25 +134,39 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF } handler = UndertowCompressionConfigurer.configureCompression(getCompression(), handler); + Closeable closeable = null; if (isAccessLogEnabled()) { - handler = createAccessLogHandler(handler); + closeable = configureAccessLogHandler(builder, handler); } - return handler; + else { + builder.setHandler(handler); + } + return closeable; } - private AccessLogHandler createAccessLogHandler( - io.undertow.server.HttpHandler handler) { + private Closeable configureAccessLogHandler(Undertow.Builder builder, + HttpHandler handler) { try { createAccessLogDirectoryIfNecessary(); + XnioWorker worker = createWorker(); String prefix = (this.accessLogPrefix != null ? this.accessLogPrefix : "access_log."); - AccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver( - createWorker(), this.accessLogDirectory, prefix, this.accessLogSuffix, + DefaultAccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver( + worker, this.accessLogDirectory, prefix, this.accessLogSuffix, this.accessLogRotate); - String formatString = (this.accessLogPattern != null ? this.accessLogPattern + String formatString = ((this.accessLogPattern != null) ? this.accessLogPattern : "common"); - return new AccessLogHandler(handler, accessLogReceiver, formatString, - Undertow.class.getClassLoader()); + builder.setHandler(new AccessLogHandler(handler, accessLogReceiver, + formatString, Undertow.class.getClassLoader())); + return () -> { + try { + accessLogReceiver.close(); + worker.shutdown(); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + }; } catch (IOException ex) { throw new IllegalStateException("Failed to create AccessLogHandler", ex); 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 5b17450a960..f1664639193 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,11 +25,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EventListener; import java.util.List; import java.util.Set; import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; import javax.servlet.ServletException; import io.undertow.Undertow; @@ -48,6 +51,7 @@ 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; @@ -292,27 +296,35 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac } private void configureAccessLog(DeploymentInfo deploymentInfo) { - deploymentInfo.addInitialHandlerChainWrapper(this::createAccessLogHandler); - } - - private AccessLogHandler createAccessLogHandler(HttpHandler handler) { try { createAccessLogDirectoryIfNecessary(); + XnioWorker worker = createWorker(); String prefix = (this.accessLogPrefix != null ? this.accessLogPrefix : "access_log."); - AccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver( - createWorker(), this.accessLogDirectory, prefix, this.accessLogSuffix, + DefaultAccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver( + worker, this.accessLogDirectory, prefix, this.accessLogSuffix, this.accessLogRotate); - String formatString = (this.accessLogPattern != null ? this.accessLogPattern - : "common"); - return new AccessLogHandler(handler, accessLogReceiver, formatString, - Undertow.class.getClassLoader()); + 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()) { @@ -645,4 +657,33 @@ 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(); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + } + } 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 2efdf609cca..c5f20ed30e9 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 @@ -16,6 +16,7 @@ package org.springframework.boot.web.embedded.undertow; +import java.io.Closeable; import java.lang.reflect.Field; import java.net.BindException; import java.net.InetSocketAddress; @@ -56,6 +57,8 @@ public class UndertowWebServer implements WebServer { private final boolean autoStart; + private final Closeable closeable; + private Undertow undertow; private volatile boolean started = false; @@ -66,8 +69,21 @@ public class UndertowWebServer implements WebServer { * @param autoStart if the server should be started */ public UndertowWebServer(Undertow.Builder builder, boolean autoStart) { + this(builder, autoStart, null); + } + + /** + * Create a new {@link UndertowWebServer} instance. + * @param builder the builder + * @param autoStart if the server should be started + * @param closeable called when the server is stopped + * @since 2.0.4 + */ + public UndertowWebServer(Undertow.Builder builder, boolean autoStart, + Closeable closeable) { this.builder = builder; this.autoStart = autoStart; + this.closeable = closeable; } @Override @@ -112,6 +128,7 @@ public class UndertowWebServer implements WebServer { try { if (this.undertow != null) { this.undertow.stop(); + this.closeable.close(); } } catch (Exception ex) { @@ -214,6 +231,9 @@ public class UndertowWebServer implements WebServer { this.started = false; try { this.undertow.stop(); + if (this.closeable != null) { + this.closeable.close(); + } } catch (Exception ex) { throw new WebServerException("Unable to stop undertow", ex); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java index a57627ec036..aa956ab6345 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java @@ -16,15 +16,25 @@ package org.springframework.boot.web.embedded.undertow; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; import java.util.Arrays; import io.undertow.Undertow; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.mockito.InOrder; +import reactor.core.publisher.Mono; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests; +import org.springframework.http.MediaType; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -38,6 +48,9 @@ import static org.mockito.Mockito.mock; public class UndertowReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Override protected UndertowReactiveWebServerFactory getFactory() { return new UndertowReactiveWebServerFactory(0); @@ -83,4 +96,44 @@ public class UndertowReactiveWebServerFactoryTests assertForwardHeaderIsUsed(factory); } + @Test + public void accessLogCanBeEnabled() + throws IOException, URISyntaxException, InterruptedException { + testAccessLog(null, null, "access_log.log"); + } + + @Test + public void accessLogCanBeCustomized() + throws IOException, URISyntaxException, InterruptedException { + testAccessLog("my_access.", "logz", "my_access.logz"); + } + + private void testAccessLog(String prefix, String suffix, String expectedFile) + throws IOException, URISyntaxException, InterruptedException { + UndertowReactiveWebServerFactory factory = getFactory(); + factory.setAccessLogEnabled(true); + factory.setAccessLogPrefix(prefix); + factory.setAccessLogSuffix(suffix); + File accessLogDirectory = this.temporaryFolder.getRoot(); + factory.setAccessLogDirectory(accessLogDirectory); + assertThat(accessLogDirectory.listFiles()).isEmpty(); + this.webServer = factory.getWebServer(new EchoHandler()); + this.webServer.start(); + WebClient client = getWebClient().build(); + Mono result = client.post().uri("/test").contentType(MediaType.TEXT_PLAIN) + .body(BodyInserters.fromObject("Hello World")).exchange() + .flatMap((response) -> response.bodyToMono(String.class)); + assertThat(result.block()).isEqualTo("Hello World"); + File accessLog = new File(accessLogDirectory, expectedFile); + awaitFile(accessLog); + assertThat(accessLogDirectory.listFiles()).contains(accessLog); + } + + private void awaitFile(File file) throws InterruptedException { + long end = System.currentTimeMillis() + 10000; + while (!file.exists() && System.currentTimeMillis() < end) { + Thread.sleep(100); + } + } + }