From c9be84af6eef0f53cd712eaac67311ccd6271f4d Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 14 Jan 2026 15:48:34 +0100 Subject: [PATCH] Delete web server's temporary directories when the context is closed Closes gh-9983 --- .../MyCloudFoundryConfiguration.java | 4 +- .../MyCloudFoundryConfiguration.kt | 4 +- .../servlet/JettyServletWebServerFactory.java | 41 ++++++++++++-- .../boot/tomcat/TomcatWebServerFactory.java | 39 ++++++++++++- .../TomcatReactiveWebServerFactory.java | 30 ++++++++-- .../TomcatServletWebServerFactory.java | 27 ++++++++- .../TomcatReactiveWebServerFactoryTests.java | 8 ++- .../TomcatServletWebServerFactoryTests.java | 8 ++- .../AbstractConfigurableWebServerFactory.java | 55 +++++++++++++++++++ ...ractConfigurableWebServerFactoryTests.java | 50 +++++++++++++++++ 10 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactoryTests.java diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java index 3474d0b8bcf..7c5171599a3 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java @@ -43,8 +43,8 @@ public class MyCloudFoundryConfiguration { return new TomcatServletWebServerFactory() { @Override - protected void prepareContext(Host host, ServletContextInitializer[] initializers) { - super.prepareContext(host, initializers); + protected void prepareContext(Host host, ServletContextInitializer[] initializers, TempDirs tempDirs) { + super.prepareContext(host, initializers, tempDirs); StandardContext child = new StandardContext(); child.addLifecycleListener(new Tomcat.FixContextListener()); child.setPath("/cloudfoundryapplication"); diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.kt index 82346ce9b58..3800ae8d1ff 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.kt @@ -41,8 +41,8 @@ class MyCloudFoundryConfiguration { fun servletWebServerFactory(): TomcatServletWebServerFactory { return object : TomcatServletWebServerFactory() { - override fun prepareContext(host: Host, initializers: Array) { - super.prepareContext(host, initializers) + override fun prepareContext(host: Host, initializers: Array, tempDirs: TempDirs) { + super.prepareContext(host, initializers, tempDirs) val child = StandardContext() child.addLifecycleListener(FixContextListener()) child.path = "/cloudfoundryapplication" diff --git a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java index b673c8d2a80..aeadd1de85d 100644 --- a/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java +++ b/module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/servlet/JettyServletWebServerFactory.java @@ -63,6 +63,7 @@ import org.eclipse.jetty.session.DefaultSessionCache; import org.eclipse.jetty.session.FileSessionDataStore; import org.eclipse.jetty.session.SessionConfig; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.resource.URLResourceFactory; @@ -161,7 +162,9 @@ public class JettyServletWebServerFactory extends JettyWebServerFactory InetSocketAddress address = new InetSocketAddress(getAddress(), port); Server server = createServer(address); context.setServer(server); - configureWebAppContext(context, initializers); + TempDirs tempDirs = new TempDirs(port); + server.addEventListener(new CleanTempDirsListener(tempDirs)); + configureWebAppContext(context, tempDirs, initializers); server.setHandler(addHandlerWrappers(context)); logger.info("Server initialized with port: " + port); if (this.getMaxConnections() > -1) { @@ -214,8 +217,23 @@ public class JettyServletWebServerFactory extends JettyWebServerFactory * Configure the given Jetty {@link WebAppContext} for use. * @param context the context to configure * @param initializers the set of initializers to apply + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #configureWebAppContext(WebAppContext, TempDirs, ServletContextInitializer...)} */ + @Deprecated(forRemoval = true, since = "4.1.0") protected final void configureWebAppContext(WebAppContext context, ServletContextInitializer... initializers) { + configureWebAppContext(context, new TempDirs(getPort()), initializers); + } + + /** + * Configure the given Jetty {@link WebAppContext} for use. + * @param context the context to configure + * @param tempDirs to manage temporary directories + * @param initializers the set of initializers to apply + * @since 4.1.0 + */ + protected final void configureWebAppContext(WebAppContext context, TempDirs tempDirs, + ServletContextInitializer... initializers) { Assert.notNull(context, "'context' must not be null"); context.clearAliasChecks(); if (this.resourceLoader != null) { @@ -224,7 +242,7 @@ public class JettyServletWebServerFactory extends JettyWebServerFactory String contextPath = getSettings().getContextPath().toString(); context.setContextPath(StringUtils.hasLength(contextPath) ? contextPath : "/"); context.setDisplayName(getSettings().getDisplayName()); - configureDocumentRoot(context); + configureDocumentRoot(context, tempDirs); if (getSettings().isRegisterDefaultServlet()) { addDefaultServlet(context); } @@ -289,11 +307,11 @@ public class JettyServletWebServerFactory extends JettyWebServerFactory } } - private void configureDocumentRoot(WebAppContext handler) { + private void configureDocumentRoot(WebAppContext handler, TempDirs tempDirs) { DocumentRoot documentRoot = new DocumentRoot(logger); documentRoot.setDirectory(this.settings.getDocumentRoot()); File root = documentRoot.getValidDirectory(); - File docBase = (root != null) ? root : createTempDir("jetty-docbase"); + File docBase = (root != null) ? root : tempDirs.createTempDir("jetty-docbase").toFile(); try { ResourceFactory resourceFactory = handler.getResourceFactory(); List resources = new ArrayList<>(); @@ -616,4 +634,19 @@ public class JettyServletWebServerFactory extends JettyWebServerFactory } + private static class CleanTempDirsListener implements LifeCycle.Listener { + + private final TempDirs tempDirs; + + CleanTempDirsListener(TempDirs tempDirs) { + this.tempDirs = tempDirs; + } + + @Override + public void lifeCycleStopped(LifeCycle event) { + this.tempDirs.cleanup(); + } + + } + } diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/TomcatWebServerFactory.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/TomcatWebServerFactory.java index ffa6d90f1b4..87af56d4fee 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/TomcatWebServerFactory.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/TomcatWebServerFactory.java @@ -29,6 +29,8 @@ import java.util.Set; import org.apache.catalina.Context; import org.apache.catalina.Engine; import org.apache.catalina.Executor; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; import org.apache.catalina.Valve; import org.apache.catalina.connector.Connector; @@ -367,16 +369,34 @@ public class TomcatWebServerFactory extends AbstractConfigurableWebServerFactory this.useApr = useApr; } + /** + * Creates the {@link Tomcat} web server. + * @return the web server. + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #createTomcat(TempDirs)} + */ + @Deprecated(forRemoval = true, since = "4.1.0") protected Tomcat createTomcat() { + return createTomcat(new TempDirs(getPort())); + } + + /** + * Creates the {@link Tomcat} web server. + * @param tempDirs to manage temporary directories + * @return the web server + * @since 4.1.0 + */ + protected Tomcat createTomcat(TempDirs tempDirs) { if (this.isDisableMBeanRegistry()) { Registry.disableRegistry(); } Tomcat tomcat = new Tomcat(); - File baseDir = (getBaseDirectory() != null) ? getBaseDirectory() : createTempDir("tomcat"); + File baseDir = (getBaseDirectory() != null) ? getBaseDirectory() : tempDirs.createTempDir("tomcat").toFile(); tomcat.setBaseDir(baseDir.getAbsolutePath()); for (LifecycleListener listener : getDefaultServerLifecycleListeners()) { tomcat.getServer().addLifecycleListener(listener); } + tomcat.getServer().addLifecycleListener(new CleanTempDirsListener(tempDirs)); Connector connector = new Connector(getProtocol()); connector.setThrowOnFailure(true); tomcat.getService().addConnector(connector); @@ -463,4 +483,21 @@ public class TomcatWebServerFactory extends AbstractConfigurableWebServerFactory } } + private static class CleanTempDirsListener implements LifecycleListener { + + private final TempDirs tempDirs; + + CleanTempDirsListener(TempDirs tempDirs) { + this.tempDirs = tempDirs; + } + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) { + this.tempDirs.cleanup(); + } + } + + } + } diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactory.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactory.java index 0d52aa95c49..dcc82cb91c3 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactory.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactory.java @@ -16,7 +16,7 @@ package org.springframework.boot.tomcat.reactive; -import java.io.File; +import java.nio.file.Path; import org.apache.catalina.Context; import org.apache.catalina.Host; @@ -68,20 +68,40 @@ public class TomcatReactiveWebServerFactory extends TomcatWebServerFactory @Override public WebServer getWebServer(HttpHandler httpHandler) { - Tomcat tomcat = createTomcat(); + TempDirs tempDirs = new TempDirs(getPort()); + Tomcat tomcat = createTomcat(tempDirs); TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler); - prepareContext(tomcat.getHost(), servlet); + prepareContext(tomcat.getHost(), servlet, tempDirs); return getTomcatWebServer(tomcat); } + /** + * Prepares the context. + * @param host the host + * @param servlet the servlet adapter + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #prepareContext(Host, TomcatHttpHandlerAdapter, TempDirs)} + */ + @Deprecated(forRemoval = true, since = "4.1.0") protected void prepareContext(Host host, TomcatHttpHandlerAdapter servlet) { - File docBase = createTempDir("tomcat-docbase"); + prepareContext(host, servlet, new TempDirs(getPort())); + } + + /** + * Prepares the context. + * @param host the host + * @param servlet the servlet adapter + * @param tempDirs to manage temporary directories + * @since 4.1.0 + */ + protected void prepareContext(Host host, TomcatHttpHandlerAdapter servlet, TempDirs tempDirs) { + Path docBase = tempDirs.createTempDir("tomcat-docbase"); TomcatEmbeddedContext context = new TomcatEmbeddedContext(); WebResourceRoot resourceRoot = new StandardRoot(context); ignoringNoSuchMethodError(() -> resourceRoot.setReadOnly(true)); context.setResources(resourceRoot); context.setPath(""); - context.setDocBase(docBase.getAbsolutePath()); + context.setDocBase(docBase.toAbsolutePath().toString()); context.addLifecycleListener(new Tomcat.FixContextListener()); ClassLoader parentClassLoader = ClassUtils.getDefaultClassLoader(); context.setParentClassLoader(parentClassLoader); diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactory.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactory.java index 5297b1fb096..6f0fa10b30b 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactory.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactory.java @@ -161,12 +161,32 @@ public class TomcatServletWebServerFactory extends TomcatWebServerFactory @Override public WebServer getWebServer(ServletContextInitializer... initializers) { - Tomcat tomcat = createTomcat(); - prepareContext(tomcat.getHost(), initializers); + TempDirs tempDirs = new TempDirs(getPort()); + Tomcat tomcat = createTomcat(tempDirs); + prepareContext(tomcat.getHost(), initializers, tempDirs); return getTomcatWebServer(tomcat); } + /** + * Prepares the context. + * @param host the host + * @param initializers the servlet context initializers + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of + * {@link #prepareContext(Host, ServletContextInitializer[], TempDirs)} + */ + @Deprecated(forRemoval = true, since = "4.1.0") protected void prepareContext(Host host, ServletContextInitializer[] initializers) { + prepareContext(host, initializers, new TempDirs(getPort())); + } + + /** + * Prepares the context. + * @param host the host + * @param initializers the servlet context initializers + * @param tempDirs to manage temporary directories + * @since 4.1.0 + */ + protected void prepareContext(Host host, ServletContextInitializer[] initializers, TempDirs tempDirs) { DocumentRoot documentRoot = new DocumentRoot(logger); documentRoot.setDirectory(this.settings.getDocumentRoot()); File documentRootFile = documentRoot.getValidDirectory(); @@ -179,7 +199,8 @@ public class TomcatServletWebServerFactory extends TomcatWebServerFactory context.setName(contextPath); context.setDisplayName(this.settings.getDisplayName()); context.setPath(contextPath); - File docBase = (documentRootFile != null) ? documentRootFile : createTempDir("tomcat-docbase"); + File docBase = (documentRootFile != null) ? documentRootFile + : tempDirs.createTempDir("tomcat-docbase").toFile(); context.setDocBase(docBase.getAbsolutePath()); context.addLifecycleListener(new FixContextListener()); ClassLoader parentClassLoader = (this.resourceLoader != null) ? this.resourceLoader.getClassLoader() diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java index 60d0bb69d0c..c9fff9eccec 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/reactive/TomcatReactiveWebServerFactoryTests.java @@ -104,7 +104,9 @@ class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFacto assertThat(factory.getContextLifecycleListeners()).isEmpty(); TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(mock(HttpHandler.class)); this.webServer = tomcatWebServer; - assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).isEmpty(); + assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()) + .extracting((l) -> l.getClass().getSimpleName()) + .containsExactly("CleanTempDirsListener"); } @Test @@ -113,8 +115,8 @@ class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFacto factory.setUseApr(true); TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(mock(HttpHandler.class)); this.webServer = tomcatWebServer; - assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).singleElement() - .isInstanceOf(AprLifecycleListener.class); + assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).extracting(Object::getClass) + .contains(AprLifecycleListener.class); } @Test diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java index 6d493800d39..32b1c453798 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java @@ -160,7 +160,9 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory assertThat(factory.getContextLifecycleListeners()).isEmpty(); TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(); this.webServer = tomcatWebServer; - assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).isEmpty(); + assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()) + .extracting((listener) -> listener.getClass().getSimpleName()) + .containsExactly("CleanTempDirsListener"); } @Test @@ -169,8 +171,8 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory factory.setUseApr(true); TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(); this.webServer = tomcatWebServer; - assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).singleElement() - .isInstanceOf(AprLifecycleListener.class); + assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).extracting(Object::getClass) + .contains(AprLifecycleListener.class); } @Test diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java index 2b1ca3ce3d6..579232bda42 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java @@ -20,7 +20,9 @@ import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -33,6 +35,7 @@ import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.error.ErrorPage; import org.springframework.boot.web.server.Ssl.ServerNameSslBundle; import org.springframework.util.Assert; +import org.springframework.util.FileSystemUtils; /** * Abstract base class for {@link ConfigurableWebServerFactory} implementations. @@ -215,7 +218,9 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab * Return the absolute temp dir for given web server. * @param prefix server name * @return the temp dir for given server. + * @deprecated since 4.1.0 for removal in 4.3.0 in favor of {@link TempDirs}. */ + @Deprecated(since = "4.1.0", forRemoval = true) protected final File createTempDir(String prefix) { try { File tempDir = Files.createTempDirectory(prefix + "." + getPort() + ".").toFile(); @@ -228,4 +233,54 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab } } + /** + * Manages temporary directories. + * + * @since 4.1.0 + */ + public static class TempDirs { + + private final Set dirs = new HashSet<>(); + + private final int port; + + public TempDirs(int port) { + this.port = port; + } + + /** + * Creates a temporary directory and registers it for {@link #cleanup cleanup}. + * @param prefix the directory prefix + * @return the path to the temporary directory + */ + public Path createTempDir(String prefix) { + try { + Path directory = Files.createTempDirectory(prefix + "." + this.port + ".").toAbsolutePath(); + this.dirs.add(directory); + return directory; + } + catch (IOException ex) { + throw new WebServerException( + "Unable to create tempDir. java.io.tmpdir is set to " + System.getProperty("java.io.tmpdir"), + ex); + } + } + + /** + * Deletes all created temporary directories. + */ + public void cleanup() { + for (Path dir : this.dirs) { + try { + FileSystemUtils.deleteRecursively(dir); + } + catch (IOException ex) { + // Ignore + } + } + this.dirs.clear(); + } + + } + } diff --git a/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactoryTests.java b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactoryTests.java new file mode 100644 index 00000000000..450f776d4c7 --- /dev/null +++ b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactoryTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.web.server; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory.TempDirs; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AbstractConfigurableWebServerFactory}. + * + * @author Moritz Halbritter + */ +class AbstractConfigurableWebServerFactoryTests { + + @Test + void shouldCleanUpTempDirs() throws IOException { + TempDirs tempDirs = new TempDirs(1234); + Path tempDir1 = tempDirs.createTempDir(getClass().getSimpleName()); + Path tempDir2 = tempDirs.createTempDir(getClass().getSimpleName()); + assertThat(tempDir1).isNotEqualTo(tempDir2); + assertThat(tempDir1).exists(); + assertThat(tempDir2).exists(); + Files.writeString(tempDir1.resolve("test.txt"), "test"); + tempDirs.cleanup(); + assertThat(tempDir1).doesNotExist(); + assertThat(tempDir2).doesNotExist(); + } + +}