diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfiguration.java index 82e014ae53..7259724781 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfiguration.java @@ -17,16 +17,17 @@ package org.springframework.boot.actuate.autoconfigure.kubernetes; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.availability.LivenessProbeHealthIndicator; +import org.springframework.boot.actuate.availability.ReadinessProbeHealthIndicator; import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer; -import org.springframework.boot.actuate.kubernetes.LivenessProbeHealthIndicator; import org.springframework.boot.actuate.kubernetes.ProbesHealthEndpointGroupsRegistrar; -import org.springframework.boot.actuate.kubernetes.ReadinessProbeHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; -import org.springframework.boot.autoconfigure.kubernetes.ApplicationStateAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.availability.ApplicationAvailabilityProvider; import org.springframework.boot.cloud.CloudPlatform; -import org.springframework.boot.kubernetes.ApplicationStateProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -39,21 +40,23 @@ import org.springframework.context.annotation.Configuration; */ @Configuration(proxyBeanMethods = false) @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) -@AutoConfigureAfter(ApplicationStateAutoConfiguration.class) +@AutoConfigureAfter(ApplicationAvailabilityAutoConfiguration.class) public class ProbesHealthContributorAutoConfiguration { @Bean @ConditionalOnEnabledHealthIndicator("livenessProbe") + @ConditionalOnMissingBean public LivenessProbeHealthIndicator livenessProbeHealthIndicator( - ApplicationStateProvider applicationStateProvider) { - return new LivenessProbeHealthIndicator(applicationStateProvider); + ApplicationAvailabilityProvider applicationAvailabilityProvider) { + return new LivenessProbeHealthIndicator(applicationAvailabilityProvider); } @Bean @ConditionalOnEnabledHealthIndicator("readinessProbe") + @ConditionalOnMissingBean public ReadinessProbeHealthIndicator readinessProbeHealthIndicator( - ApplicationStateProvider applicationStateProvider) { - return new ReadinessProbeHealthIndicator(applicationStateProvider); + ApplicationAvailabilityProvider applicationAvailabilityProvider) { + return new ReadinessProbeHealthIndicator(applicationAvailabilityProvider); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfigurationTests.java new file mode 100644 index 0000000000..57e0c937d5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfigurationTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2020 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.actuate.autoconfigure.kubernetes; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.availability.LivenessProbeHealthIndicator; +import org.springframework.boot.actuate.availability.ReadinessProbeHealthIndicator; +import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration; +import org.springframework.boot.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests fos {@link ProbesHealthContributorAutoConfiguration}. + * + * @author Brian Clozel + */ +class ProbesHealthContributorAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations + .of(ApplicationAvailabilityAutoConfiguration.class, ProbesHealthContributorAutoConfiguration.class)); + + @Test + void probesNotConfiguredIfNotKubernetes() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class) + .doesNotHaveBean(LivenessProbeHealthIndicator.class) + .doesNotHaveBean(ReadinessProbeHealthIndicator.class) + .doesNotHaveBean(HealthEndpointGroupsRegistryCustomizer.class)); + } + + @Test + void probesConfiguredIfKubernetes() { + this.contextRunner.withPropertyValues("spring.main.cloud-platform=kubernetes") + .run((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class) + .hasSingleBean(LivenessProbeHealthIndicator.class) + .hasSingleBean(ReadinessProbeHealthIndicator.class) + .hasSingleBean(HealthEndpointGroupsRegistryCustomizer.class)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicator.java similarity index 68% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicator.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicator.java index 77a7da544c..b8b3c9bad5 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicator.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.boot.actuate.kubernetes; +package org.springframework.boot.actuate.availability; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.kubernetes.ApplicationStateProvider; -import org.springframework.boot.kubernetes.LivenessState; +import org.springframework.boot.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.availability.LivenessState; /** * A {@link HealthIndicator} that checks the {@link LivenessState} of the application. @@ -30,15 +30,15 @@ import org.springframework.boot.kubernetes.LivenessState; */ public class LivenessProbeHealthIndicator extends AbstractHealthIndicator { - private final ApplicationStateProvider applicationStateProvider; + private final ApplicationAvailabilityProvider applicationAvailabilityProvider; - public LivenessProbeHealthIndicator(ApplicationStateProvider applicationStateProvider) { - this.applicationStateProvider = applicationStateProvider; + public LivenessProbeHealthIndicator(ApplicationAvailabilityProvider applicationAvailabilityProvider) { + this.applicationAvailabilityProvider = applicationAvailabilityProvider; } @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - if (LivenessState.live().equals(this.applicationStateProvider.getLivenessState())) { + if (LivenessState.live().equals(this.applicationAvailabilityProvider.getLivenessState())) { builder.up(); } else { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicator.java similarity index 68% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicator.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicator.java index 3af8598083..ca785fe634 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicator.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.boot.actuate.kubernetes; +package org.springframework.boot.actuate.availability; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.kubernetes.ApplicationStateProvider; -import org.springframework.boot.kubernetes.ReadinessState; +import org.springframework.boot.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.availability.ReadinessState; /** * A {@link HealthIndicator} that checks the {@link ReadinessState} of the application. @@ -30,15 +30,15 @@ import org.springframework.boot.kubernetes.ReadinessState; */ public class ReadinessProbeHealthIndicator extends AbstractHealthIndicator { - private final ApplicationStateProvider applicationStateProvider; + private final ApplicationAvailabilityProvider applicationAvailabilityProvider; - public ReadinessProbeHealthIndicator(ApplicationStateProvider applicationStateProvider) { - this.applicationStateProvider = applicationStateProvider; + public ReadinessProbeHealthIndicator(ApplicationAvailabilityProvider applicationAvailabilityProvider) { + this.applicationAvailabilityProvider = applicationAvailabilityProvider; } @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - if (ReadinessState.ready().equals(this.applicationStateProvider.getReadinessState())) { + if (ReadinessState.ready().equals(this.applicationAvailabilityProvider.getReadinessState())) { builder.up(); } else { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/package-info.java new file mode 100644 index 0000000000..44ce413ce2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2019 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. + */ + +/** + * Actuator support for application availability concerns. + */ +package org.springframework.boot.actuate.availability; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicatorTests.java similarity index 83% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicatorTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicatorTests.java index 96086eb1e1..3b39c74c29 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicatorTests.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.actuate.kubernetes; +package org.springframework.boot.actuate.availability; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.kubernetes.ApplicationStateProvider; -import org.springframework.boot.kubernetes.LivenessState; +import org.springframework.boot.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.availability.LivenessState; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.when; @@ -34,13 +34,13 @@ import static org.mockito.Mockito.mock; */ class LivenessProbeHealthIndicatorTests { - private ApplicationStateProvider stateProvider; + private ApplicationAvailabilityProvider stateProvider; private LivenessProbeHealthIndicator healthIndicator; @BeforeEach void setUp() { - this.stateProvider = mock(ApplicationStateProvider.class); + this.stateProvider = mock(ApplicationAvailabilityProvider.class); this.healthIndicator = new LivenessProbeHealthIndicator(this.stateProvider); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicatorTests.java similarity index 80% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicatorTests.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicatorTests.java index 43d11add83..7abfa279b5 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicatorTests.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.actuate.kubernetes; +package org.springframework.boot.actuate.availability; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.kubernetes.ApplicationStateProvider; -import org.springframework.boot.kubernetes.ReadinessState; +import org.springframework.boot.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.availability.ReadinessState; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.when; @@ -34,13 +34,13 @@ import static org.mockito.Mockito.mock; */ class ReadinessProbeHealthIndicatorTests { - private ApplicationStateProvider stateProvider; + private ApplicationAvailabilityProvider stateProvider; private ReadinessProbeHealthIndicator healthIndicator; @BeforeEach void setUp() { - this.stateProvider = mock(ApplicationStateProvider.class); + this.stateProvider = mock(ApplicationAvailabilityProvider.class); this.healthIndicator = new ReadinessProbeHealthIndicator(this.stateProvider); } @@ -51,8 +51,8 @@ class ReadinessProbeHealthIndicatorTests { } @Test - void readinessIsBusy() { - when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.busy()); + void readinessIsUnready() { + when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.unready()); assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.OUT_OF_SERVICE); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.java similarity index 56% rename from spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java rename to spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.java index 1fa4d8f3d6..32f6289403 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.java @@ -14,34 +14,25 @@ * limitations under the License. */ -package org.springframework.boot.autoconfigure.kubernetes; +package org.springframework.boot.autoconfigure.availability; -import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; -import org.springframework.boot.cloud.CloudPlatform; -import org.springframework.boot.kubernetes.ApplicationStateProvider; -import org.springframework.boot.kubernetes.SpringApplicationEventListener; +import org.springframework.boot.availability.ApplicationAvailabilityProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration} for - * {@link ApplicationStateProvider}. + * {@link ApplicationAvailabilityProvider}. * * @author Brian Clozel * @since 2.3.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) -public class ApplicationStateAutoConfiguration { +public class ApplicationAvailabilityAutoConfiguration { @Bean - public ApplicationStateProvider applicationStateProvider() { - return new ApplicationStateProvider(); - } - - @Bean - public SpringApplicationEventListener springApplicationEventListener() { - return new SpringApplicationEventListener(); + public ApplicationAvailabilityProvider applicationAvailabilityProvider() { + return new ApplicationAvailabilityProvider(); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/package-info.java similarity index 83% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/package-info.java rename to spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/package-info.java index 43205acafa..2904d14e4b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/package-info.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/package-info.java @@ -15,6 +15,6 @@ */ /** - * Support for Kubernetes deployment environments. + * Auto-configuration for application availability features. */ -package org.springframework.boot.kubernetes; +package org.springframework.boot.autoconfigure.availability; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 00b9500ff1..c5342b9149 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -89,7 +89,7 @@ org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\ org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\ org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\ org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\ -org.springframework.boot.autoconfigure.kubernetes.ApplicationStateAutoConfiguration,\ +org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\ org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\ org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\ org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java similarity index 55% rename from spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java rename to spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java index 5934efdccf..3e598c37f6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java @@ -19,33 +19,25 @@ package org.springframework.boot.autoconfigure.kubernetes; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.kubernetes.ApplicationStateProvider; -import org.springframework.boot.kubernetes.SpringApplicationEventListener; +import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration; +import org.springframework.boot.availability.ApplicationAvailabilityProvider; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link ApplicationStateAutoConfiguration} + * Tests for {@link ApplicationAvailabilityAutoConfiguration} * * @author Brian Clozel */ -class ApplicationStateAutoConfigurationTests { +class ApplicationAvailabilityAutoConfigurationTests { private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ApplicationStateAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(ApplicationAvailabilityAutoConfiguration.class)); @Test - void disabledWhenNotDeployedOnKubernetes() { - this.contextRunner.run(((context) -> assertThat(context).doesNotHaveBean(ApplicationStateProvider.class) - .doesNotHaveBean(SpringApplicationEventListener.class))); - } - - @Test - void enabledWhenDeployedOnKubernetes() { - this.contextRunner.withPropertyValues("spring.main.cloud-platform:kubernetes") - .run(((context) -> assertThat(context).hasSingleBean(ApplicationStateProvider.class) - .hasSingleBean(SpringApplicationEventListener.class))); + void providerIsPresent() { + this.contextRunner.run(((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class))); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfigurationTests.java index c756a21f81..7fb8aac6cf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfigurationTests.java @@ -92,7 +92,9 @@ class WebSocketMessagingAutoConfigurationTests { @AfterEach void tearDown() { - this.context.close(); + if (this.context.isActive()) { + this.context.close(); + } this.sockJsClient.stop(); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc index 9f5ca67f6c..328cde83c7 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc @@ -184,8 +184,27 @@ TIP: The https://github.com/pivotal-cf/java-cfenv/[Java CFEnv] project is a bett [[cloud-deployment-kubernetes]] === Kubernetes Spring Boot auto-detects Kubernetes deployment environments by checking the environment for `"*_SERVICE_HOST"` and `"*_SERVICE_PORT"` variables. +You can override this detection with the configprop:spring.main.cloud-platform[] configuration property. -Spring Boot helps you to <> and export it with <>. +Spring Boot helps you to <> and export it with <>. + +[[cloud-deployment-kubernetes-container-lifecycle]] +==== Kubernetes Container Lifecycle +When Kubernetes deletes an application instance, the shutdown process involves several subsystems concurrently: shutdown hooks, unregistering the service, removing the instance from the load-balancer... +Because this shutdown processing happens in parallel (and due to the nature of distributed systems), there is a window during which traffic can be routed to a pod that has also begun its shutdown processing. + +You can configure a sleep execution in a pre-stop hook to avoid requests being routed to a pod that has already begun shutting down. +This sleep should be long enough for new requests to stop being routed to the pod and its duration will vary from deployment to deployment. + +[source,yml,indent=0] +---- +lifecycle: + preStop: + exec: + command: ["sh", "-c", "sleep 10"] +---- + +Once the pre-stop hook has completed, SIGTERM will be sent to the container and <> will begin, allowing any remaining in-flight requests to complete. [[cloud-deployment-heroku]] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc index db819a1e36..ee38a80c96 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc @@ -875,33 +875,109 @@ TIP: You can use `@Qualifier("groupname")` if you need to register custom `Statu Applications deployed on Kubernetes can provide information about their internal state with https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes[Container Probes]. Depending on https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[your Kubernetes configuration], the kubelet will call those probes and react to the result. -When deployed in a Kubernetes environment, Spring Boot will manage your <>. -Spring Boot Actuator will gather the "Liveness" and "Readiness" information using <> +Spring Boot manages your <> out-of-the-box. +If deployed in a Kubernetes environment, Actuator will gather the "Liveness" and "Readiness" information from the `ApplicationAvailabilityProvider`, create dedicated <> and expose them as HTTP Probes using <>. -You can then point your Kubernetes `"livenessProbe"` to `"/actuator/health/liveness"` and `"readinessProbe"` to `"/actuator/health/readiness"`. -If your application context startup can be longer than the configured `"livenessProbe"` timeout, configuring a `"startupProbe"` can solve startup issues. +You can then configure your Kubernetes infrastructure with the following endpoint information: -TIP: Application should perform long-running startup tasks with `ApplicationRunners`; in this case, you don't necessarily need a `"startupProbe"` as such tasks are performed after the "Liveness" state is marked as successful. -The application won't receive traffic until tasks are finished - only then the `"readinessProbe"` will be successful. +[source,yml,indent=0] +---- +livenessProbe: + httpGet: + path: /actuator/health/liveness + port: liveness-port + failureThreshold: ... + periodSeconds: ... + +readinessProbe: + httpGet: + path: /actuator/health/readiness + port: liveness-port + failureThreshold: ... + periodSeconds: ... +---- + +NOTE: If an application takes longer to start than the configured liveness period, Kubernetes mention the `"startupProbe"` as a possible solution. +The `"startupProbe"` is not necessarily needed here as the `"readinessProbe"` fails until all startup tasks are done, see <>. + +WARNING: If your Actuator endpoints are deployed on a separate management context, be aware that endpoints are then not using the same web infrastructure (port, connection pools, framework components) as the main application. +In this case, a Probe check could be successful even if the main application does not work properly (for example, it cannot accept new connections). -As explained in the <>, depending on external systems for defining the "Liveness" state might result in cascading failures. -In case your application needs to rely on a specific subset of health checks for "Liveness" or "Readiness", you should consider configuring <> manually: +[[production-ready-kubernetes-probes-external-state]] +==== Checking external state with Kubernetes Probes +Actuator configures the "liveness" and "readiness" Probes as Health Groups; this means that all the <> are available for them. +You can, for example, configure additional Health Indicators: [source,properties,indent=0,configprops] ---- management.endpoint.health.group.readiness.include=readinessProbe,customCheck ---- -WARNING: In general, "Liveness" and "Readiness" probes should avoid being based on external checks, such as <>. -If an external system fails (e.g. a database, a Web API, an external cache), Kubernetes would react by restarting application instances or spinning up many new instances. -You should carefully consider external checks and how the platform should handle such failures. +By default, Spring Boot does not add other Health Indicators to these groups. + +The "liveness" Probe should not depend on health checks for external systems. +If the <> is broken, Kubernetes will try to solve that problem by restarting the application instance. +This means that if an external system fails (e.g. a database, a Web API, an external cache), Kubernetes might restart all application instances and create cascading failures. + +As for the "readiness" Probe, the choice of checking external systems should be made carefully by the application developers. +If the <> is unready, Kubernetes will not route traffic to that application instance. +Some external systems might not be shared by application instances or not essential to the application (the application could have circuit breakers and fallbacks). +Also, Kubernetes will react differently to applications being taken out of the load-balancer, depending on its https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/[autoscaling configuration]. -WARNING: If your Actuator endpoints are deployed on a separate management context, be aware that endpoints are then not using the same web infrastructure (port, connection pools, framework components) as the main application. -In this case, a Probe check could be successful even if the main application does not work properly (for example, it cannot accept new connections). -You'll find more information in the <> section. +[[production-ready-kubernetes-probes-lifecycle]] +==== Application lifecycle and Probes states +An important aspect of the Kubernetes Probes support is its consistency with the application lifecycle. +Spring Boot publishes <>. + +When a Spring Boot application starts: + +[cols="3,2,2,6"] +|=== +|Application startup phase |Liveness State |Readiness State |Notes + +|Starting +|broken +|unready +|Kubernetes checks the "liveness" Probe and restarts the application if it takes too long. + +|Started +|live +|unready +|The application context is refreshed. The application performs startup tasks and does not receive traffic yet. + +|Ready +|live +|ready +|Startup tasks are finished. The application is receiving traffic. + +|=== + +When a Spring Boot application shuts down: + +[cols="3,2,2,6"] +|=== +|Application shutdown phase |Liveness State |Readiness State |Notes + +|Running +|live +|ready +|Shutdown has been requested. + +|Graceful shutdown +|live +|unready +|If enabled, <>. + +|Shutdown complete +|broken +|unready +|The application context is closed and the application cannot serve traffic. + +|=== +TIP: Check out the <> for more information about Kubernetes deployment. [[production-ready-application-info]] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index 2ba300e78c..a62a2c0ef0 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -191,6 +191,90 @@ NOTE: There are some restrictions when creating an `ApplicationContext` hierarch For example, Web components *must* be contained within the child context, and the same `Environment` is used for both parent and child contexts. See the {spring-boot-module-api}/builder/SpringApplicationBuilder.html[`SpringApplicationBuilder` Javadoc] for full details. +[[boot-features-application-availability-state]] +== Application Availability State +When deployed on plaftorms, applications can provide information about their availability to the platform using infrastructure like https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[Kubernetes Probes]. +Spring Boot manages this application state with the `ApplicationAvailabilityProvider` and makes it available to application components and the platform itself. + + +[[boot-features-application-availability-liveness]] +=== Liveness State +The "Liveness" state of an application tells whether its internal state allows it to work correctly, or recover by itself if it's currently failing. +An invalid "Liveness" state means that the application is in a broken state and cannot recover from it; the infrastructure should then restart the application to mitigate that problem. + +NOTE: In general, the "Liveness" state should not be based on external checks, such as <>. +If it did, a failing external system (a database, a Web API, an external cache) would trigger massive restarts and cascading failures across the platform. + +The internal state of Spring Boot applications is mostly represented by the Spring application context. +If the application context has started successfully, Spring Boot assumes that the application is in a valid state. +An application is considered live as soon as the context has been refreshed, see <>. + +[[boot-features-application-availability-readiness]] +=== Readiness State +The "Readiness" state of an application tells whether the application is ready to handle traffic. +A failing "Readiness" state tells the platform that it should not route traffic to the application for now. +This typically happens during startup, while `CommandLineRunner` and `ApplicationRunner` components are being processed, or at any time if the application decides that it's too busy for additional traffic. + +An application is considered ready as soon as application and command-line runners have been called, see <>. + +TIP: Tasks expected to run during startup should be executed by `CommandLineRunner` and `ApplicationRunner` components instead of using Spring component lifecycle callbacks such as `@PostConstruct`. + + + +[[boot-features-managing-application-availability]] +=== Managing the Application Availability State +Application components can retrieve the current availability state at any time, by injecting the `ApplicationAvailabilityProvider` and calling methods on it. +More often, applications will want to listen to state updates or update the state of the application. + +For example, we can export the "Readiness" state of the application to a file so that a Kubernetes "exec Probe" can look at this file: + +[source,java,indent=0] +---- + @Component + public class ReadinessStateExporter implements ApplicationListener { + + @Override + public void onApplicationEvent(LivenessStateChangedEvent event) { + switch (event.getReadinessState().getStatus()) { + case READY: + // create file /tmp/healthy + break; + case UNREADY: + // remove file /tmp/healthy + break; + } + } + + } +---- + +We can also update the state of the application, when the application breaks and cannot recover: + +[source,java,indent=0] +---- + @Component + public class LocalCacheVerifier { + + private final ApplicationEventPublisher eventPublisher; + + public LocalCacheVerifier(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + public void checkLocalCache() { + try { + //... + } + catch (CacheCompletelyBrokenException ex) { + this.eventPublisher.publishEvent(LivenessStateChangedEvent.broken(ex)); + } + } + + } +---- + +Spring Boot provides <>. +You can get more guidance about <>. [[boot-features-application-events-and-listeners]] @@ -218,8 +302,9 @@ Application events are sent in the following order, as your application runs: . An `ApplicationContextInitializedEvent` is sent when the `ApplicationContext` is prepared and ApplicationContextInitializers have been called but before any bean definitions are loaded. . An `ApplicationPreparedEvent` is sent just before the refresh is started but after bean definitions have been loaded. . An `ApplicationStartedEvent` is sent after the context has been refreshed but before any application and command-line runners have been called. +. An `LivenessStateChangedEvent` is sent right after to indicate that the application is considered as live. . An `ApplicationReadyEvent` is sent after any application and command-line runners have been called. - It indicates that the application is ready to service requests. +. An `ReadinessStateChangedEvent` is sent right after to indicate that the application is ready to service requests. . An `ApplicationFailedEvent` is sent if there is an exception on startup. The above list only includes ``SpringApplicationEvent``s that are tied to a `SpringApplication`. @@ -6013,91 +6098,6 @@ For instance, it is possible to customize the name of the table for the JDBC sto For setting the timeout of the session you can use the configprop:spring.session.timeout[] property. If that property is not set, the auto-configuration falls back to the value of configprop:server.servlet.session.timeout[]. -[[boot-features-kubernetes-application-state]] -== Kubernetes Application State -When deployed on Kubernetes, applications can provide information about their state to the platform using https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[Kubernetes Probes]. -Spring Boot manages this application state with the `ApplicationStateProvider` and makes it available to application components and the cloud platform itself. -This feature is only enabled when the application is running on Kubernetes. - -TIP: The cloud platform detection relies on platform-specific environment variables, but you can override this detection with the configprop:spring.main.cloud-platform[] configuration property. - -[[boot-features-kubernetes-liveness-state]] -=== Liveness State -The "Liveness" state of an application tells whether its internal state allows it to work correctly, or recover by itself if it's currently failing. -An invalid "Liveness" state means that the application is in a broken state and cannot recover from it; the infrastructure should then restart the application to mitigate that problem. - -NOTE: In general, the "Liveness" state should not be based external checks, such as <>. -If it did, a failing external system (a database, a Web API, an external cache) would trigger massive restarts and cascading failures across the platform. - -The internal state of Spring Boot applications is mostly represented by the Spring application context. -If the application context has started successfully, Spring Boot assumes that the application is in a valid state. -An application is considered live as soon as the `ApplicationStartedEvent` has been published, see <>. - -[[boot-features-kubernetes-readiness-state]] -=== Readiness State -The "Readiness" state of an application tells whether the application is ready to handle traffic. -A failing "Readiness" state tells Kubernetes that it should not route traffic to the application for now. -This typically happens during startup, while `CommandLineRunner` and `ApplicationRunner` components are being processed, or at any time if the application decides that it's too busy for additional traffic. - -An application is considered ready as soon as the `ApplicationReadyEvent` has been published, see <>. - -TIP: Tasks expected to run during startup should be executed by `CommandLineRunner` and `ApplicationRunner` components instead of using Spring component lifecycle callbacks such as `@PostConstruct`. - -[[boot-features-kubernetes-managing-state]] -=== Managing Kubernetes Application State -Application components can retrieve the current application state at any time, by injecting the `ApplicationStateProvider` and calling methods on it. -More often, applications will want to listen to state updates or update the state of the application. - -For example, we can export the "Readiness" state of the application to a file so that a Kubernetes "exec Probe" can look at this file: - -[source,java,indent=0] ----- - @Component - public class LivenessStateExporter implements ApplicationListener { - - @Override - public void onApplicationEvent(LivenessStateChangedEvent event) { - switch (event.getLivenessState().getStatus()) { - case LIVE: - // create file /tmp/healthy - break; - case BROKEN: - // remove file /tmp/healthy - break; - } - } - - } ----- - -We can also update the state of the application, when the application breaks and cannot recover: - -[source,java,indent=0] ----- - @Component - public class LocalCacheVerifier { - - private final ApplicationEventPublisher eventPublisher; - - public LocalCacheVerifier(ApplicationEventPublisher eventPublisher) { - this.eventPublisher = eventPublisher; - } - - public void checkLocalCache() { - try { - //... - } - catch (CacheCompletelyBroken ex) { - this.eventPublisher.publishEvent(LivenessStateChangedEvent.broken(ex)); - } - } - - } ----- - -Spring Boot provides <>. -You can get more guidance about <>. - [[boot-features-jmx]] == Monitoring and Management over JMX diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ApplicationStateProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityProvider.java similarity index 78% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ApplicationStateProvider.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityProvider.java index f4fb54095a..0819d548a0 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ApplicationStateProvider.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityProvider.java @@ -14,33 +14,33 @@ * limitations under the License. */ -package org.springframework.boot.kubernetes; +package org.springframework.boot.availability; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; /** - * Holds the application state, when deployed in a Kubernetes environment. + * Holds the availability state of the application. *

* Other application components can get the current state information from the - * {@code ApplicationStateProvider}, or publish application evens such as + * {@code ApplicationAvailabilityProvider}, or publish application evens such as * {@link ReadinessStateChangedEvent} and {@link LivenessStateChangedEvent} to update the * state of the application. * * @author Brian Clozel * @since 2.3.0 */ -public class ApplicationStateProvider implements ApplicationListener { +public class ApplicationAvailabilityProvider implements ApplicationListener { private LivenessState livenessState; private ReadinessState readinessState; - public ApplicationStateProvider() { - this(LivenessState.broken(), ReadinessState.busy()); + public ApplicationAvailabilityProvider() { + this(LivenessState.broken(), ReadinessState.unready()); } - public ApplicationStateProvider(LivenessState livenessState, ReadinessState readinessState) { + public ApplicationAvailabilityProvider(LivenessState livenessState, ReadinessState readinessState) { this.livenessState = livenessState; this.readinessState = readinessState; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessState.java similarity index 94% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessState.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessState.java index 8545e9114b..800dde4285 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessState.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessState.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.kubernetes; +package org.springframework.boot.availability; import java.util.Objects; /** - * "Liveness" state of the application, when deployed on Kubernetes. + * "Liveness" state of the application. *

* An application is considered live when it's running with a correct internal state. * "Liveness" failure means that the internal state of the application is broken and we diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessStateChangedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessStateChangedEvent.java similarity index 83% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessStateChangedEvent.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessStateChangedEvent.java index c2f5ae7dae..7d27a8a0f7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessStateChangedEvent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessStateChangedEvent.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.kubernetes; +package org.springframework.boot.availability; import org.springframework.context.ApplicationEvent; @@ -40,6 +40,16 @@ public class LivenessStateChangedEvent extends ApplicationEvent { return (LivenessState) getSource(); } + /** + * Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is + * live. + * @param cause the cause of the live internal state of the application + * @return the application event + */ + public static LivenessStateChangedEvent live(String cause) { + return new LivenessStateChangedEvent(LivenessState.live(), cause); + } + /** * Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is * broken. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessState.java similarity index 90% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessState.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessState.java index aedca872bc..f2fe3ece5b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessState.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessState.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.boot.kubernetes; +package org.springframework.boot.availability; import java.util.Objects; /** - * "Readiness" state of the application, when deployed on Kubernetes. + * "Readiness" state of the application. *

* An application is considered ready when it's {@link LivenessState live} and willing to * accept traffic. "Readiness" failure means that the application is not able to accept @@ -66,8 +66,8 @@ public final class ReadinessState { return new ReadinessState(Availability.READY); } - public static ReadinessState busy() { - return new ReadinessState(Availability.BUSY); + public static ReadinessState unready() { + return new ReadinessState(Availability.UNREADY); } public enum Availability { @@ -75,7 +75,7 @@ public final class ReadinessState { /** * The application is not willing to receive traffic. */ - BUSY, + UNREADY, /** * The application is ready to receive traffic. */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessStateChangedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessStateChangedEvent.java similarity index 89% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessStateChangedEvent.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessStateChangedEvent.java index 83b4bc108d..d26d345415 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessStateChangedEvent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessStateChangedEvent.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.kubernetes; +package org.springframework.boot.availability; import org.springframework.context.ApplicationEvent; @@ -48,11 +48,11 @@ public class ReadinessStateChangedEvent extends ApplicationEvent { /** * Create a new {@code ApplicationEvent} signaling that the {@link ReadinessState} is - * busy. + * unready. * @return the application event */ - public static ReadinessStateChangedEvent busy() { - return new ReadinessStateChangedEvent(ReadinessState.busy()); + public static ReadinessStateChangedEvent unready() { + return new ReadinessStateChangedEvent(ReadinessState.unready()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/package-info.java similarity index 84% rename from spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/package-info.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/package-info.java index bf6feee5dd..5bacc7b1f3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/package-info.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/package-info.java @@ -15,6 +15,6 @@ */ /** - * Auto-configuration for Kubernetes application features. + * Support for describing the availability of Spring Boot applications. */ -package org.springframework.boot.autoconfigure.kubernetes; +package org.springframework.boot.availability; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java index ce91bec648..86d55ac3b3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java @@ -21,6 +21,8 @@ import org.apache.commons.logging.LogFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplicationRunListener; +import org.springframework.boot.availability.LivenessStateChangedEvent; +import org.springframework.boot.availability.ReadinessStateChangedEvent; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; @@ -41,6 +43,7 @@ import org.springframework.util.ErrorHandler; * @author Stephane Nicoll * @author Andy Wilkinson * @author Artsiom Yudovin + * @author Brian Clozel * @since 1.0.0 */ public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered { @@ -96,11 +99,13 @@ public class EventPublishingRunListener implements SpringApplicationRunListener, @Override public void started(ConfigurableApplicationContext context) { context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context)); + context.publishEvent(LivenessStateChangedEvent.live("Application started")); } @Override public void running(ConfigurableApplicationContext context) { context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context)); + context.publishEvent(ReadinessStateChangedEvent.ready()); } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/SpringApplicationEventListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/SpringApplicationEventListener.java deleted file mode 100644 index 1fbc5bfc39..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/SpringApplicationEventListener.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2012-2020 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.kubernetes; - -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.boot.context.event.ApplicationStartedEvent; -import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextClosedEvent; - -/** - * {@link ApplicationListener} that listens for application lifecycle events such as - * {@link ApplicationStartedEvent}, {@link ApplicationReadyEvent}, - * {@link ContextClosedEvent}. Those events are then translated and published into events - * consumed by {@link ApplicationStateProvider} to update the application state. - * - * @author Brian Clozel - * @since 2.3.0 - */ -public class SpringApplicationEventListener - implements ApplicationListener, ApplicationEventPublisherAware { - - private ApplicationEventPublisher applicationEventPublisher; - - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { - this.applicationEventPublisher = applicationEventPublisher; - } - - @Override - public void onApplicationEvent(ApplicationEvent event) { - if (event instanceof ApplicationStartedEvent) { - LivenessState livenessState = LivenessState.live(); - this.applicationEventPublisher - .publishEvent(new LivenessStateChangedEvent(livenessState, "Application has started")); - } - else if (event instanceof ApplicationReadyEvent) { - this.applicationEventPublisher.publishEvent(ReadinessStateChangedEvent.ready()); - } - else if (event instanceof ContextClosedEvent) { - this.applicationEventPublisher.publishEvent(ReadinessStateChangedEvent.busy()); - } - } - -} 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 4d243969b9..700da37529 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 @@ -22,6 +22,7 @@ import reactor.core.publisher.Mono; import org.springframework.beans.BeansException; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.availability.ReadinessStateChangedEvent; import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.server.WebServer; @@ -148,6 +149,7 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli @Override protected void doClose() { + publishEvent(ReadinessStateChangedEvent.unready()); WebServer webServer = getWebServer(); if (webServer != null) { webServer.shutDownGracefully(); 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 797b212351..0b793071d9 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 @@ -37,6 +37,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.Scope; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.availability.ReadinessStateChangedEvent; import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -168,6 +169,7 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon @Override protected void doClose() { + publishEvent(ReadinessStateChangedEvent.unready()); WebServer webServer = this.webServer; if (webServer != null) { webServer.shutDownGracefully(); 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 40959c5274..9c86797962 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 @@ -46,6 +46,8 @@ import org.springframework.beans.factory.support.BeanDefinitionOverrideException import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; +import org.springframework.boot.availability.LivenessStateChangedEvent; +import org.springframework.boot.availability.ReadinessStateChangedEvent; import org.springframework.boot.context.event.ApplicationContextInitializedEvent; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationFailedEvent; @@ -408,7 +410,9 @@ class SpringApplicationTests { inOrder.verify(listener).onApplicationEvent(isA(ApplicationPreparedEvent.class)); inOrder.verify(listener).onApplicationEvent(isA(ContextRefreshedEvent.class)); inOrder.verify(listener).onApplicationEvent(isA(ApplicationStartedEvent.class)); + inOrder.verify(listener).onApplicationEvent(isA(LivenessStateChangedEvent.class)); inOrder.verify(listener).onApplicationEvent(isA(ApplicationReadyEvent.class)); + inOrder.verify(listener).onApplicationEvent(isA(ReadinessStateChangedEvent.class)); inOrder.verifyNoMoreInteractions(); } @@ -903,7 +907,7 @@ class SpringApplicationTests { ApplicationListener listener = this.context.getBean("testApplicationListener", ApplicationListener.class); verifyListenerEvents(listener, ContextRefreshedEvent.class, ApplicationStartedEvent.class, - ApplicationReadyEvent.class); + LivenessStateChangedEvent.class, ApplicationReadyEvent.class, ReadinessStateChangedEvent.class); } @SuppressWarnings("unchecked") diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/ApplicationStateProviderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/ApplicationAvailabilityProviderTests.java similarity index 76% rename from spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/ApplicationStateProviderTests.java rename to spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/ApplicationAvailabilityProviderTests.java index 4edd2e4574..d2458f56cc 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/ApplicationStateProviderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/ApplicationAvailabilityProviderTests.java @@ -14,29 +14,29 @@ * limitations under the License. */ -package org.springframework.boot.kubernetes; +package org.springframework.boot.availability; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link ApplicationStateProvider} + * Tests for {@link ApplicationAvailabilityProvider} * * @author Brian Clozel */ -class ApplicationStateProviderTests { +class ApplicationAvailabilityProviderTests { @Test void initialStateShouldBeFailures() { - ApplicationStateProvider stateProvider = new ApplicationStateProvider(); + ApplicationAvailabilityProvider stateProvider = new ApplicationAvailabilityProvider(); assertThat(stateProvider.getLivenessState()).isEqualTo(LivenessState.broken()); - assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.busy()); + assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.unready()); } @Test void updateLivenessState() { - ApplicationStateProvider stateProvider = new ApplicationStateProvider(); + ApplicationAvailabilityProvider stateProvider = new ApplicationAvailabilityProvider(); LivenessState livenessState = LivenessState.live(); stateProvider.onApplicationEvent(new LivenessStateChangedEvent(livenessState, "Startup complete")); assertThat(stateProvider.getLivenessState()).isEqualTo(livenessState); @@ -44,7 +44,7 @@ class ApplicationStateProviderTests { @Test void updateReadiessState() { - ApplicationStateProvider stateProvider = new ApplicationStateProvider(); + ApplicationAvailabilityProvider stateProvider = new ApplicationAvailabilityProvider(); stateProvider.onApplicationEvent(ReadinessStateChangedEvent.ready()); assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.ready()); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/event/EventPublishingRunListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/event/EventPublishingRunListenerTests.java new file mode 100644 index 0000000000..dba0a7fce0 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/event/EventPublishingRunListenerTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2020 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.context.event; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.availability.LivenessStateChangedEvent; +import org.springframework.boot.availability.ReadinessStateChangedEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.support.StaticApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link EventPublishingRunListener} + * + * @author Brian Clozel + */ +class EventPublishingRunListenerTests { + + private SpringApplication application; + + private EventPublishingRunListener runListener; + + private TestApplicationListener eventListener; + + @BeforeEach + void setup() { + this.eventListener = new TestApplicationListener(); + this.application = mock(SpringApplication.class); + given(this.application.getListeners()).willReturn(Collections.singleton(this.eventListener)); + this.runListener = new EventPublishingRunListener(this.application, null); + } + + @Test + void shouldPublishLifecyleEvents() { + StaticApplicationContext context = new StaticApplicationContext(); + assertThat(this.eventListener.receivedEvents()).isEmpty(); + this.runListener.starting(); + checkApplicationEvents(ApplicationStartingEvent.class); + this.runListener.environmentPrepared(null); + checkApplicationEvents(ApplicationEnvironmentPreparedEvent.class); + this.runListener.contextPrepared(context); + checkApplicationEvents(ApplicationContextInitializedEvent.class); + this.runListener.contextLoaded(context); + checkApplicationEvents(ApplicationPreparedEvent.class); + context.refresh(); + this.runListener.started(context); + checkApplicationEvents(ApplicationStartedEvent.class, LivenessStateChangedEvent.class); + this.runListener.running(context); + checkApplicationEvents(ApplicationReadyEvent.class, ReadinessStateChangedEvent.class); + } + + void checkApplicationEvents(Class... eventClasses) { + assertThat(this.eventListener.receivedEvents()).extracting("class").contains((Object[]) eventClasses); + } + + static class TestApplicationListener implements ApplicationListener { + + private Deque events = new ArrayDeque<>(); + + @Override + public void onApplicationEvent(ApplicationEvent event) { + this.events.add(event); + } + + List receivedEvents() { + List receivedEvents = new ArrayList<>(); + while (!this.events.isEmpty()) { + receivedEvents.add(this.events.pollFirst()); + } + return receivedEvents; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/SpringApplicationEventListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/SpringApplicationEventListenerTests.java deleted file mode 100644 index 3ce5b1c5de..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/SpringApplicationEventListenerTests.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2012-2020 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.kubernetes; - -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.boot.context.event.ApplicationStartedEvent; -import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.event.ContextClosedEvent; -import org.springframework.context.support.StaticApplicationContext; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link SpringApplicationEventListener} - * - * @author Brian Clozel - */ -class SpringApplicationEventListenerTests { - - @Test - void shouldReactToApplicationStartedEvent() { - ApplicationEvent event = publishAndReceiveApplicationEvent( - new ApplicationStartedEvent(new SpringApplication(), null, null)); - - assertThat(event).isInstanceOf(LivenessStateChangedEvent.class); - LivenessStateChangedEvent livenessEvent = (LivenessStateChangedEvent) event; - assertThat(livenessEvent.getLivenessState()).isEqualTo(LivenessState.live()); - } - - @Test - void shouldReactToApplicationReadyEvent() { - ApplicationEvent event = publishAndReceiveApplicationEvent( - new ApplicationReadyEvent(new SpringApplication(), null, null)); - - assertThat(event).isInstanceOf(ReadinessStateChangedEvent.class); - ReadinessStateChangedEvent readinessEvent = (ReadinessStateChangedEvent) event; - assertThat(readinessEvent.getReadinessState()).isEqualTo(ReadinessState.ready()); - } - - @Test - void shouldReactToContextClosedEvent() { - ApplicationEvent event = publishAndReceiveApplicationEvent( - new ContextClosedEvent(new StaticApplicationContext())); - - assertThat(event).isInstanceOf(ReadinessStateChangedEvent.class); - ReadinessStateChangedEvent readinessEvent = (ReadinessStateChangedEvent) event; - assertThat(readinessEvent.getReadinessState()).isEqualTo(ReadinessState.busy()); - } - - private ApplicationEvent publishAndReceiveApplicationEvent(ApplicationEvent eventToSend) { - ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class); - SpringApplicationEventListener eventListener = new SpringApplicationEventListener(); - eventListener.setApplicationEventPublisher(eventPublisher); - - eventListener.onApplicationEvent(eventToSend); - ArgumentCaptor event = ArgumentCaptor.forClass(ApplicationEvent.class); - verify(eventPublisher).publishEvent(event.capture()); - - return event.getValue(); - } - -} 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 562962962d..ce6927d4d8 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 @@ -16,7 +16,11 @@ package org.springframework.boot.web.servlet.context; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; import java.util.EnumSet; +import java.util.List; import java.util.Properties; import javax.servlet.DispatcherType; @@ -45,6 +49,7 @@ import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.Scope; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.boot.availability.ReadinessStateChangedEvent; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer; @@ -54,7 +59,10 @@ import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.server.MockServletWebServerFactory; import org.springframework.context.ApplicationContextException; +import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -134,12 +142,14 @@ class ServletWebServerApplicationContextTests { @Test void ServletWebServerInitializedEventPublished() { addWebServerFactoryBean(); - this.context.registerBeanDefinition("listener", new RootBeanDefinition(MockListener.class)); + this.context.registerBeanDefinition("listener", new RootBeanDefinition(TestApplicationListener.class)); this.context.refresh(); - ServletWebServerInitializedEvent event = this.context.getBean(MockListener.class).getEvent(); - assertThat(event).isNotNull(); - assertThat(event.getSource().getPort() >= 0).isTrue(); - assertThat(event.getApplicationContext()).isEqualTo(this.context); + List events = this.context.getBean(TestApplicationListener.class).receivedEvents(); + assertThat(events).hasSize(2).extracting("class").contains(ContextRefreshedEvent.class, + ServletWebServerInitializedEvent.class); + ServletWebServerInitializedEvent initializedEvent = (ServletWebServerInitializedEvent) events.get(1); + assertThat(initializedEvent.getSource().getPort() >= 0).isTrue(); + assertThat(initializedEvent.getApplicationContext()).isEqualTo(this.context); } @Test @@ -161,6 +171,17 @@ class ServletWebServerApplicationContextTests { verify(factory.getWebServer()).stop(); } + @Test + void applicationIsUnreadyDuringShutdown() { + TestApplicationListener listener = new TestApplicationListener(); + addWebServerFactoryBean(); + this.context.refresh(); + this.context.addApplicationListener(listener); + this.context.close(); + assertThat(listener.receivedEvents()).hasSize(2).extracting("class").contains(ReadinessStateChangedEvent.class, + ContextClosedEvent.class); + } + @Test void cannotSecondRefresh() { addWebServerFactoryBean(); @@ -468,17 +489,21 @@ class ServletWebServerApplicationContextTests { return object; } - static class MockListener implements ApplicationListener { + static class TestApplicationListener implements ApplicationListener { - private ServletWebServerInitializedEvent event; + private Deque events = new ArrayDeque<>(); @Override - public void onApplicationEvent(ServletWebServerInitializedEvent event) { - this.event = event; + public void onApplicationEvent(ApplicationEvent event) { + this.events.add(event); } - ServletWebServerInitializedEvent getEvent() { - return this.event; + List receivedEvents() { + List receivedEvents = new ArrayList<>(); + while (!this.events.isEmpty()) { + receivedEvents.add(this.events.pollFirst()); + } + return receivedEvents; } }