diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 1c82413a11..8caf83662d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -786,6 +786,7 @@ public class SpringApplication { reportFailure(getExceptionReporters(context), exception); if (context != null) { context.close(); + shutdownHook.deregisterFailedApplicationContext(context); } } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java index 632688dd63..debde8af7a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java @@ -43,6 +43,7 @@ import org.springframework.util.Assert; * * @author Andy Wilkinson * @author Phillip Webb + * @author Brian Clozel */ class SpringApplicationShutdownHook implements Runnable { @@ -92,6 +93,17 @@ class SpringApplicationShutdownHook implements Runnable { } } + void deregisterFailedApplicationContext(ConfigurableApplicationContext applicationContext) { + synchronized (SpringApplicationShutdownHook.class) { + if (!applicationContext.isActive()) { + SpringApplicationShutdownHook.this.contexts.remove(applicationContext); + } + else { + throw new IllegalStateException("Cannot unregister active application context"); + } + } + } + @Override public void run() { Set contexts; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java index aed3d89ed2..2671b13e1e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -26,6 +26,7 @@ import java.util.concurrent.CountDownLatch; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -36,12 +37,14 @@ import org.springframework.context.support.GenericApplicationContext; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link SpringApplicationShutdownHook}. * * @author Phillip Webb * @author Andy Wilkinson + * @author Brian Clozel */ class SpringApplicationShutdownHookTests { @@ -154,6 +157,29 @@ class SpringApplicationShutdownHookTests { .withMessage("Shutdown in progress"); } + @Test + void failsWhenDeregisterActiveContext() { + TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook(); + ConfigurableApplicationContext context = new GenericApplicationContext(); + shutdownHook.registerApplicationContext(context); + context.refresh(); + assertThatThrownBy(() -> shutdownHook.deregisterFailedApplicationContext(context)) + .isInstanceOf(IllegalStateException.class); + assertThat(shutdownHook.isApplicationContextRegistered(context)).isTrue(); + } + + @Test + void deregistersFailedContext() { + TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook(); + GenericApplicationContext context = new GenericApplicationContext(); + shutdownHook.registerApplicationContext(context); + context.registerBean(FailingBean.class); + assertThatThrownBy(context::refresh).isInstanceOf(BeanCreationException.class); + assertThat(shutdownHook.isApplicationContextRegistered(context)).isTrue(); + shutdownHook.deregisterFailedApplicationContext(context); + assertThat(shutdownHook.isApplicationContextRegistered(context)).isFalse(); + } + static class TestSpringApplicationShutdownHook extends SpringApplicationShutdownHook { private boolean runtimeShutdownHookAdded; @@ -259,4 +285,13 @@ class SpringApplicationShutdownHookTests { } + static class FailingBean implements InitializingBean { + + @Override + public void afterPropertiesSet() throws Exception { + throw new IllegalArgumentException("test failure"); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 6a4d6e5c07..ffb1cff6f9 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -16,10 +16,12 @@ package org.springframework.boot; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -150,6 +152,7 @@ import static org.mockito.Mockito.spy; * @author Marten Deinum * @author Nguyen Bao Sach * @author Chris Bono + * @author Brian Clozel */ @ExtendWith(OutputCaptureExtension.class) class SpringApplicationTests { @@ -1247,6 +1250,20 @@ class SpringApplicationTests { assertThat(application.getEnvironmentPrefix()).isEqualTo("my"); } + @Test + void deregistersShutdownHookForFailedApplicationContext() { + SpringApplication application = new SpringApplication(BrokenPostConstructConfig.class); + List events = new ArrayList<>(); + application.addListeners(events::add); + application.setWebApplicationType(WebApplicationType.NONE); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(application::run); + assertThat(events).hasAtLeastOneElementOfType(ApplicationFailedEvent.class); + ApplicationFailedEvent failure = events.stream().filter((event) -> event instanceof ApplicationFailedEvent) + .map(ApplicationFailedEvent.class::cast).findFirst().get(); + assertThat(SpringApplicationShutdownHookInstance.get()) + .didNotRegisterApplicationContext(failure.getApplicationContext()); + } + private ArgumentMatcher isAvailabilityChangeEventWithState( S state) { return (argument) -> (argument instanceof AvailabilityChangeEvent)