diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java index 9ff12bd8238d..8c787dd995aa 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java @@ -65,13 +65,18 @@ public final void refresh() throws BeansException, IllegalStateException { try { super.refresh(); } - catch (RuntimeException ex) { + catch (RuntimeException refreshEx) { WebServer webServer = getWebServer(); if (webServer != null) { - webServer.stop(); - webServer.destroy(); + try { + webServer.stop(); + webServer.destroy(); + } + catch (RuntimeException stopOrDestroyEx) { + refreshEx.addSuppressed(stopOrDestroyEx); + } } - throw ex; + throw refreshEx; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java index 6562e68d1e8f..2555919b9572 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java @@ -145,13 +145,18 @@ public final void refresh() throws BeansException, IllegalStateException { try { super.refresh(); } - catch (RuntimeException ex) { - WebServer webServer = this.webServer; - if (webServer != null) { - webServer.stop(); - webServer.destroy(); + catch (RuntimeException refreshEx) { + try { + WebServer webServer = this.webServer; + if (webServer != null) { + webServer.stop(); + webServer.destroy(); + } } - throw ex; + catch (RuntimeException stopOrDestroyEx) { + refreshEx.addSuppressed(stopOrDestroyEx); + } + throw refreshEx; } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContextTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContextTests.java index e87deafa73ca..1c91af3fe14b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContextTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContextTests.java @@ -43,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.times; /** @@ -133,6 +134,40 @@ void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyed() { then(webServer).should().destroy(); } + @Test + void whenContextRefreshFailedThenWebServerStopFailedCatchStopException() { + addWebServerFactoryBean(); + addHttpHandlerBean(); + this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> { + willThrow(new RuntimeException("WebServer has failed to stop")).willCallRealMethod() + .given(this.context.getWebServer()) + .stop(); + return new RefreshFailure(); + })); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh) + .withStackTraceContaining("WebServer has failed to stop"); + WebServer webServer = this.context.getWebServer(); + then(webServer).should().stop(); + then(webServer).should(times(0)).destroy(); + } + + @Test + void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyFailedCatchDestroyException() { + addWebServerFactoryBean(); + addHttpHandlerBean(); + this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> { + willThrow(new RuntimeException("WebServer has failed to destroy")).willCallRealMethod() + .given(this.context.getWebServer()) + .destroy(); + return new RefreshFailure(); + })); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh) + .withStackTraceContaining("WebServer has failed to destroy"); + WebServer webServer = this.context.getWebServer(); + then(webServer).should().stop(); + then(webServer).should().destroy(); + } + @Test void whenContextIsClosedThenWebServerIsStoppedAndDestroyed() { addWebServerFactoryBean(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java index 694fb3828e4e..d474256bf607 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -51,6 +51,7 @@ import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer; +import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletContextInitializer; @@ -81,6 +82,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -211,6 +213,48 @@ void whenContextIsNotActiveThenCloseDoesNotChangeTheApplicationAvailability() { assertThat(listener.receivedEvents()).isEmpty(); } + @Test + void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyed() { + addWebServerFactoryBean(); + this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class)); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh); + WebServer webServer = this.context.getWebServer(); + then(webServer).should(times(2)).stop(); + then(webServer).should().destroy(); + } + + @Test + void whenContextRefreshFailedThenWebServerStopFailedCatchStopException() { + addWebServerFactoryBean(); + this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> { + willThrow(new RuntimeException("WebServer has failed to stop")).willCallRealMethod() + .given(this.context.getWebServer()) + .stop(); + return new RefreshFailure(); + })); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh) + .withStackTraceContaining("WebServer has failed to stop"); + WebServer webServer = this.context.getWebServer(); + then(webServer).should().stop(); + then(webServer).should(times(0)).destroy(); + } + + @Test + void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyFailedCatchDestroyException() { + addWebServerFactoryBean(); + this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> { + willThrow(new RuntimeException("WebServer has failed to destroy")).willCallRealMethod() + .given(this.context.getWebServer()) + .destroy(); + return new RefreshFailure(); + })); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh) + .withStackTraceContaining("WebServer has failed to destroy"); + WebServer webServer = this.context.getWebServer(); + then(webServer).should().stop(); + then(webServer).should().destroy(); + } + @Test void cannotSecondRefresh() { addWebServerFactoryBean();