diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializer.java index 9ffab2263a..e21e486cab 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import jakarta.servlet.ServletContextEvent; import jakarta.servlet.ServletException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import reactor.core.scheduler.Schedulers; import org.springframework.boot.SpringApplication; import org.springframework.boot.builder.ParentContextApplicationContextInitializer; @@ -44,6 +45,7 @@ import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.web.WebApplicationInitializer; import org.springframework.web.context.ConfigurableWebEnvironment; import org.springframework.web.context.ContextLoaderListener; @@ -69,11 +71,15 @@ import org.springframework.web.context.WebApplicationContext; * @author Dave Syer * @author Phillip Webb * @author Andy Wilkinson + * @author Brian Clozel * @since 2.0.0 * @see #configure(SpringApplicationBuilder) */ public abstract class SpringBootServletInitializer implements WebApplicationInitializer { + private static final boolean REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.scheduler.Schedulers", + SpringBootServletInitializer.class.getClassLoader()); + protected Log logger; // Don't initialize early private boolean registerErrorPageFilter = true; @@ -125,6 +131,20 @@ public abstract class SpringBootServletInitializer implements WebApplicationInit } } + /** + * Shuts down the reactor {@link Schedulers} that were initialized by + * {@code Schedulers.boundedElastic()} (or similar). The default implementation + * {@link Schedulers#shutdownNow()} schedulers if they were initialized on this web + * application's class loader. + * @param servletContext the web application's servlet context + * @since 3.4.0 + */ + protected void shutDownSharedReactorSchedulers(ServletContext servletContext) { + if (Schedulers.class.getClassLoader() == servletContext.getClassLoader()) { + Schedulers.shutdownNow(); + } + } + protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) { SpringApplicationBuilder builder = createSpringApplicationBuilder(); builder.main(getClass()); @@ -248,6 +268,10 @@ public abstract class SpringBootServletInitializer implements WebApplicationInit finally { // Use original context so that the classloader can be accessed deregisterJdbcDrivers(this.servletContext); + // Shut down shared reactor schedulers tied to this classloader + if (REACTOR_PRESENT) { + shutDownSharedReactorSchedulers(this.servletContext); + } } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java index 60dc447cfe..bb8e7deb81 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java @@ -228,6 +228,32 @@ class SpringBootServletInitializerTests { assertThat(driversDeregistered).isTrue(); } + @Test + void whenServletContextIsDestroyedThenReactorSchedulersAreShutDown() throws ServletException { + ServletContext servletContext = mock(ServletContext.class); + given(servletContext.addFilter(any(), any(Filter.class))).willReturn(mock(Dynamic.class)); + given(servletContext.getInitParameterNames()).willReturn(new Vector().elements()); + given(servletContext.getAttributeNames()).willReturn(new Vector().elements()); + AtomicBoolean schedulersShutDown = new AtomicBoolean(); + new SpringBootServletInitializer() { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { + return builder.sources(Config.class); + } + + @Override + protected void shutDownSharedReactorSchedulers(ServletContext servletContext) { + schedulersShutDown.set(true); + } + + }.onStartup(servletContext); + ArgumentCaptor captor = ArgumentCaptor.forClass(ServletContextListener.class); + then(servletContext).should().addListener(captor.capture()); + captor.getValue().contextDestroyed(new ServletContextEvent(servletContext)); + assertThat(schedulersShutDown).isTrue(); + } + static class PropertySourceVerifyingSpringBootServletInitializer extends SpringBootServletInitializer { @Override