From bb79c847b24b71bccf8027aecf75472df241ade6 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 9 Apr 2020 16:04:25 -0700 Subject: [PATCH] Allow custom availability states Create a general purpose `AvailabilityState` interface and refactor the existing `LivenessState` and `ReadinessState` to use it. A single `AvailabilityChangeEvent` is now used to carry all availability state updates. This commit also renames `ApplicationAvailabilityProvider` to `ApplicationAvailabilityBean` and extracts an `ApplicationAvailability` interface that other beans can inject. The helps to hide the event listener method, which is really internal. Finally the state enums have been renamed as follows: - `LivenessState.LIVE` -> `LivenessState.CORRECT` - `ReadinessState.READY` -> `ReadinessState.ACCEPTING_TRAFFIC` - `ReadinessState.UNREADY` -> `ReadinessState.REFUSING_TRAFFIC` See gh-20962 --- ...besHealthContributorAutoConfiguration.java | 11 +- ...althContributorAutoConfigurationTests.java | 6 +- .../LivenessProbeHealthIndicator.java | 17 ++- .../ReadinessProbeHealthIndicator.java | 17 ++- .../LivenessProbeHealthIndicatorTests.java | 14 +-- .../ReadinessProbeHealthIndicatorTests.java | 14 +-- ...licationAvailabilityAutoConfiguration.java | 8 +- ...ionAvailabilityAutoConfigurationTests.java | 4 +- .../availability/ApplicationAvailability.java | 81 +++++++++++++ .../ApplicationAvailabilityBean.java | 73 ++++++++++++ .../ApplicationAvailabilityProvider.java | 88 -------------- .../availability/AvailabilityChangeEvent.java | 84 +++++++++++++ .../boot/availability/AvailabilityState.java | 30 +++++ .../boot/availability/LivenessState.java | 6 +- .../LivenessStateChangedEvent.java | 74 ------------ .../boot/availability/ReadinessState.java | 14 +-- .../ReadinessStateChangedEvent.java | 58 --------- .../event/EventPublishingRunListener.java | 9 +- .../ReactiveWebServerApplicationContext.java | 5 +- .../ServletWebServerApplicationContext.java | 5 +- .../boot/SpringApplicationTests.java | 75 ++++++++---- .../ApplicationAvailabilityBeanTests.java | 110 ++++++++++++++++++ .../ApplicationAvailabilityProviderTests.java | 52 --------- .../AvailabilityChangeEventTests.java | 64 ++++++++++ .../EventPublishingRunListenerTests.java | 7 +- ...rvletWebServerApplicationContextTests.java | 4 +- 26 files changed, 561 insertions(+), 369 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailability.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityBean.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityProvider.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityChangeEvent.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityState.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessStateChangedEvent.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessStateChangedEvent.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/ApplicationAvailabilityBeanTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/ApplicationAvailabilityProviderTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/AvailabilityChangeEventTests.java 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 bd01f0592ef..fd3073f2b29 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 @@ -29,7 +29,7 @@ import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.availability.ApplicationAvailability; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -50,17 +50,16 @@ public class ProbesHealthContributorAutoConfiguration { @Bean @ConditionalOnEnabledHealthIndicator("livenessProbe") @ConditionalOnMissingBean - public LivenessProbeHealthIndicator livenessProbeHealthIndicator( - ApplicationAvailabilityProvider applicationAvailabilityProvider) { - return new LivenessProbeHealthIndicator(applicationAvailabilityProvider); + public LivenessProbeHealthIndicator livenessProbeHealthIndicator(ApplicationAvailability applicationAvailability) { + return new LivenessProbeHealthIndicator(applicationAvailability); } @Bean @ConditionalOnEnabledHealthIndicator("readinessProbe") @ConditionalOnMissingBean public ReadinessProbeHealthIndicator readinessProbeHealthIndicator( - ApplicationAvailabilityProvider applicationAvailabilityProvider) { - return new ReadinessProbeHealthIndicator(applicationAvailabilityProvider); + ApplicationAvailability applicationAvailability) { + return new ReadinessProbeHealthIndicator(applicationAvailability); } @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 index fa919c537e7..3223ed18016 100644 --- 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 @@ -23,7 +23,7 @@ import org.springframework.boot.actuate.availability.ReadinessProbeHealthIndicat 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.availability.ApplicationAvailability; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -40,7 +40,7 @@ class ProbesHealthContributorAutoConfigurationTests { @Test void probesNotConfiguredIfNotKubernetes() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class) + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class) .doesNotHaveBean(LivenessProbeHealthIndicator.class) .doesNotHaveBean(ReadinessProbeHealthIndicator.class) .doesNotHaveBean(HealthEndpointGroupsRegistryCustomizer.class)); @@ -49,7 +49,7 @@ class ProbesHealthContributorAutoConfigurationTests { @Test void probesConfiguredIfProperty() { this.contextRunner.withPropertyValues("management.health.probes.enabled=true") - .run((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class) + .run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.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/availability/LivenessProbeHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicator.java index 0e0c8a3f892..af5111b16c8 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicator.java @@ -19,7 +19,8 @@ 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.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.availability.ApplicationAvailability; import org.springframework.boot.availability.LivenessState; /** @@ -30,20 +31,16 @@ import org.springframework.boot.availability.LivenessState; */ public class LivenessProbeHealthIndicator extends AbstractHealthIndicator { - private final ApplicationAvailabilityProvider applicationAvailabilityProvider; + private final ApplicationAvailability applicationAvailability; - public LivenessProbeHealthIndicator(ApplicationAvailabilityProvider applicationAvailabilityProvider) { - this.applicationAvailabilityProvider = applicationAvailabilityProvider; + public LivenessProbeHealthIndicator(ApplicationAvailability applicationAvailability) { + this.applicationAvailability = applicationAvailability; } @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - if (LivenessState.LIVE.equals(this.applicationAvailabilityProvider.getLivenessState())) { - builder.up(); - } - else { - builder.down(); - } + LivenessState state = this.applicationAvailability.getLivenessState(); + builder.status(LivenessState.CORRECT == state ? Status.UP : Status.DOWN); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicator.java index 004ec729cd8..9d64d6bd1b2 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicator.java @@ -19,7 +19,8 @@ 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.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.availability.ApplicationAvailability; import org.springframework.boot.availability.ReadinessState; /** @@ -30,20 +31,16 @@ import org.springframework.boot.availability.ReadinessState; */ public class ReadinessProbeHealthIndicator extends AbstractHealthIndicator { - private final ApplicationAvailabilityProvider applicationAvailabilityProvider; + private final ApplicationAvailability applicationAvailability; - public ReadinessProbeHealthIndicator(ApplicationAvailabilityProvider applicationAvailabilityProvider) { - this.applicationAvailabilityProvider = applicationAvailabilityProvider; + public ReadinessProbeHealthIndicator(ApplicationAvailability applicationAvailability) { + this.applicationAvailability = applicationAvailability; } @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - if (ReadinessState.READY.equals(this.applicationAvailabilityProvider.getReadinessState())) { - builder.up(); - } - else { - builder.outOfService(); - } + ReadinessState state = this.applicationAvailability.getReadinessState(); + builder.status(ReadinessState.ACCEPTING_TRAFFIC == state ? Status.UP : Status.OUT_OF_SERVICE); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicatorTests.java index b738b87082b..e609580327d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/LivenessProbeHealthIndicatorTests.java @@ -20,11 +20,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.availability.ApplicationAvailability; import org.springframework.boot.availability.LivenessState; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.when; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** @@ -34,25 +34,25 @@ import static org.mockito.Mockito.mock; */ class LivenessProbeHealthIndicatorTests { - private ApplicationAvailabilityProvider stateProvider; + private ApplicationAvailability availability; private LivenessProbeHealthIndicator healthIndicator; @BeforeEach void setUp() { - this.stateProvider = mock(ApplicationAvailabilityProvider.class); - this.healthIndicator = new LivenessProbeHealthIndicator(this.stateProvider); + this.availability = mock(ApplicationAvailability.class); + this.healthIndicator = new LivenessProbeHealthIndicator(this.availability); } @Test void livenessIsLive() { - when(this.stateProvider.getLivenessState()).thenReturn(LivenessState.LIVE); + given(this.availability.getLivenessState()).willReturn(LivenessState.CORRECT); assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.UP); } @Test void livenessIsBroken() { - when(this.stateProvider.getLivenessState()).thenReturn(LivenessState.BROKEN); + given(this.availability.getLivenessState()).willReturn(LivenessState.BROKEN); assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.DOWN); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicatorTests.java index 9036839e32d..f2669152e97 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/ReadinessProbeHealthIndicatorTests.java @@ -20,11 +20,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.availability.ApplicationAvailability; import org.springframework.boot.availability.ReadinessState; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.when; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** @@ -34,25 +34,25 @@ import static org.mockito.Mockito.mock; */ class ReadinessProbeHealthIndicatorTests { - private ApplicationAvailabilityProvider stateProvider; + private ApplicationAvailability availability; private ReadinessProbeHealthIndicator healthIndicator; @BeforeEach void setUp() { - this.stateProvider = mock(ApplicationAvailabilityProvider.class); - this.healthIndicator = new ReadinessProbeHealthIndicator(this.stateProvider); + this.availability = mock(ApplicationAvailability.class); + this.healthIndicator = new ReadinessProbeHealthIndicator(this.availability); } @Test void readinessIsReady() { - when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.READY); + given(this.availability.getReadinessState()).willReturn(ReadinessState.ACCEPTING_TRAFFIC); assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.UP); } @Test void readinessIsUnready() { - when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.UNREADY); + given(this.availability.getReadinessState()).willReturn(ReadinessState.REFUSING_TRAFFIC); 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/availability/ApplicationAvailabilityAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.java index 32f62894036..ed0e3d8a9d2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.java @@ -16,13 +16,13 @@ package org.springframework.boot.autoconfigure.availability; -import org.springframework.boot.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.availability.ApplicationAvailabilityBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration} for - * {@link ApplicationAvailabilityProvider}. + * {@link ApplicationAvailabilityBean}. * * @author Brian Clozel * @since 2.3.0 @@ -31,8 +31,8 @@ import org.springframework.context.annotation.Configuration; public class ApplicationAvailabilityAutoConfiguration { @Bean - public ApplicationAvailabilityProvider applicationAvailabilityProvider() { - return new ApplicationAvailabilityProvider(); + public ApplicationAvailabilityBean applicationAvailability() { + return new ApplicationAvailabilityBean(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java index 3e598c37f67..b3cddc7fd19 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java @@ -20,7 +20,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration; -import org.springframework.boot.availability.ApplicationAvailabilityProvider; +import org.springframework.boot.availability.ApplicationAvailability; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -37,7 +37,7 @@ class ApplicationAvailabilityAutoConfigurationTests { @Test void providerIsPresent() { - this.contextRunner.run(((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class))); + this.contextRunner.run(((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class))); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailability.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailability.java new file mode 100644 index 00000000000..3ab16c164ec --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailability.java @@ -0,0 +1,81 @@ +/* + * 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.availability; + +import org.springframework.context.ApplicationContext; + +/** + * Provides {@link AvailabilityState availability state} information for the application. + *

+ * Components can inject this class to get the current state information. To update the + * state of the application an {@link AvailabilityChangeEvent} should be + * {@link ApplicationContext#publishEvent published} to the application context with + * directly or via {@link AvailabilityChangeEvent#publish}. + * + * @author Brian Clozel + * @author Phillip Webb + * @since 2.3.0 + */ +public interface ApplicationAvailability { + + /** + * Return the {@link LivenessState} of the application. + * @return the liveness state + */ + default LivenessState getLivenessState() { + return getState(LivenessState.class, LivenessState.BROKEN); + } + + /** + * Return the {@link ReadinessState} of the application. + * @return the readiness state + */ + default ReadinessState getReadinessState() { + return getState(ReadinessState.class, ReadinessState.REFUSING_TRAFFIC); + } + + /** + * Return {@link AvailabilityState} information for the application. + * @param the state type + * @param stateType the state type + * @param defaultState the default state to return if no event of the given type has + * been published yet (must not be {@code null}. + * @return the readiness state + * @see #getState(Class) + */ + S getState(Class stateType, S defaultState); + + /** + * Return {@link AvailabilityState} information for the application. + * @param the state type + * @param stateType the state type + * @return the readiness state or {@code null} if no event of the given type has been + * published yet + * @see #getState(Class, AvailabilityState) + */ + S getState(Class stateType); + + /** + * Return the last {@link AvailabilityChangeEvent} received for a given state type. + * @param the state type + * @param stateType the state type + * @return the readiness state or {@code null} if no event of the given type has been + * published yet + */ + AvailabilityChangeEvent getLastChangeEvent(Class stateType); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityBean.java new file mode 100644 index 00000000000..279e2b09b15 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityBean.java @@ -0,0 +1,73 @@ +/* + * 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.availability; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.context.ApplicationListener; +import org.springframework.util.Assert; + +/** + * Bean that provides an {@link ApplicationAvailability} implementation by listening for + * {@link AvailabilityChangeEvent change events}. + * + * @author Brian Clozel + * @author Phillip Webb + * @since 2.3.0 + * @see ApplicationAvailability + */ +public class ApplicationAvailabilityBean + implements ApplicationAvailability, ApplicationListener> { + + private final Map, AvailabilityChangeEvent> events = new HashMap<>(); + + @Override + public S getState(Class stateType, S defaultState) { + Assert.notNull(stateType, "StateType must not be null"); + Assert.notNull(defaultState, "DefaultState must not be null"); + S state = getState(stateType); + return (state != null) ? state : defaultState; + } + + @Override + public S getState(Class stateType) { + AvailabilityChangeEvent event = getLastChangeEvent(stateType); + return (event != null) ? event.getState() : null; + } + + @Override + @SuppressWarnings("unchecked") + public AvailabilityChangeEvent getLastChangeEvent(Class stateType) { + return (AvailabilityChangeEvent) this.events.get(stateType); + } + + @Override + public void onApplicationEvent(AvailabilityChangeEvent event) { + Class stateType = getStateType(event.getState()); + this.events.put(stateType, event); + } + + @SuppressWarnings("unchecked") + private Class getStateType(AvailabilityState state) { + if (state instanceof Enum) { + return (Class) ((Enum) state).getDeclaringClass(); + } + return state.getClass(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityProvider.java deleted file mode 100644 index 4e9f9e403aa..00000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityProvider.java +++ /dev/null @@ -1,88 +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.availability; - -import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.util.Assert; - -/** - * Holds the availability state of the application. - *

- * Other application components can get the current state information from the - * {@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 ApplicationAvailabilityProvider implements ApplicationListener { - - private LivenessState livenessState; - - private ReadinessState readinessState; - - /** - * Create a new {@link ApplicationAvailabilityProvider} instance with - * {@link LivenessState#BROKEN} and {@link ReadinessState#UNREADY}. - */ - public ApplicationAvailabilityProvider() { - this(LivenessState.BROKEN, ReadinessState.UNREADY); - } - - /** - * Create a new {@link ApplicationAvailabilityProvider} with the given states. - * @param livenessState the liveness state - * @param readinessState the readiness state - */ - public ApplicationAvailabilityProvider(LivenessState livenessState, ReadinessState readinessState) { - Assert.notNull(livenessState, "LivenessState must not be null"); - Assert.notNull(readinessState, "ReadinessState must not be null"); - this.livenessState = livenessState; - this.readinessState = readinessState; - } - - /** - * Return the {@link LivenessState} of the application. - * @return the liveness state - */ - public LivenessState getLivenessState() { - return this.livenessState; - } - - /** - * Return the {@link ReadinessState} of the application. - * @return the readiness state - */ - public ReadinessState getReadinessState() { - return this.readinessState; - } - - @Override - public void onApplicationEvent(ApplicationEvent event) { - if (event instanceof LivenessStateChangedEvent) { - LivenessStateChangedEvent livenessEvent = (LivenessStateChangedEvent) event; - this.livenessState = livenessEvent.getLivenessState(); - } - else if (event instanceof ReadinessStateChangedEvent) { - ReadinessStateChangedEvent readinessEvent = (ReadinessStateChangedEvent) event; - this.readinessState = readinessEvent.getReadinessState(); - } - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityChangeEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityChangeEvent.java new file mode 100644 index 00000000000..ce14a5c296e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityChangeEvent.java @@ -0,0 +1,84 @@ +/* + * 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.availability; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.util.Assert; + +/** + * {@link ApplicationEvent} sent when the {@link AvailabilityState} of the application + * changes. + *

+ * Any application component can send such events to update the state of the application. + * + * @param the availability state type + * @author Brian Clozel + * @author Phillip Webb + * @since 2.3.0 + */ +public class AvailabilityChangeEvent extends ApplicationEvent { + + private final S state; + + /** + * Create a new {@link AvailabilityChangeEvent} instance. + * @param source the source of the event + * @param state the availability state (never {@code null}) + */ + public AvailabilityChangeEvent(Object source, S state) { + super(source); + Assert.notNull(state, "State must not be null"); + this.state = state; + } + + /** + * Return the changed availability state. + * @return the availability state + */ + public S getState() { + return this.state; + } + + /** + * Convenience method that can be used to publish an {@link AvailabilityChangeEvent} + * to the given application context. + * @param the availability state type + * @param context the context used to publish the event + * @param state the changed availability state + */ + public static void publish(ApplicationContext context, S state) { + Assert.notNull(context, "Context must not be null"); + publish(context, context, state); + } + + /** + * Convenience method that can be used to publish an {@link AvailabilityChangeEvent} + * to the given application context. + * @param the availability state type + * @param publisher the publisher used to publish the event + * @param source the source of the event + * @param state the changed availability state + */ + public static void publish(ApplicationEventPublisher publisher, Object source, + S state) { + Assert.notNull(publisher, "Publisher must not be null"); + publisher.publishEvent(new AvailabilityChangeEvent<>(source, state)); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityState.java new file mode 100644 index 00000000000..b78daab83b7 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityState.java @@ -0,0 +1,30 @@ +/* + * 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.availability; + +/** + * Tagging interface used on {@link ApplicationAvailability} states. This interface is + * usually implemented on an {@code enum} type. + * + * @author Phillip Webb + * @since 2.3.0 + * @see LivenessState + * @see ReadinessState + */ +public interface AvailabilityState { + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessState.java index 303eb5d47a9..885d7c27bfb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessState.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessState.java @@ -26,15 +26,15 @@ package org.springframework.boot.availability; * @author Brian Clozel * @since 2.3.0 */ -public enum LivenessState { +public enum LivenessState implements AvailabilityState { /** * The application is running and its internal state is correct. */ - LIVE, + CORRECT, /** - * The internal state of the application is broken. + * The application is running but its internal state is broken. */ BROKEN diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessStateChangedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessStateChangedEvent.java deleted file mode 100644 index 21d9ba32d5f..00000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessStateChangedEvent.java +++ /dev/null @@ -1,74 +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.availability; - -import org.springframework.context.ApplicationEvent; - -/** - * {@link ApplicationEvent} sent when the {@link LivenessState} of the application - * changes. - *

- * Any application component can send such events to update the state of the application. - * - * @author Brian Clozel - * @since 2.3.0 - */ -public class LivenessStateChangedEvent extends ApplicationEvent { - - private final String cause; - - LivenessStateChangedEvent(LivenessState state, String cause) { - super(state); - this.cause = cause; - } - - public LivenessState getLivenessState() { - 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. - * @param cause the cause of the broken internal state of the application - * @return the application event - */ - public static LivenessStateChangedEvent broken(String cause) { - return new LivenessStateChangedEvent(LivenessState.BROKEN, cause); - } - - /** - * Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is - * broken. - * @param throwable the exception that caused the broken internal state of the - * application - * @return the application event - */ - public static LivenessStateChangedEvent broken(Throwable throwable) { - return new LivenessStateChangedEvent(LivenessState.BROKEN, throwable.getMessage()); - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessState.java index 50cc39242d6..6c372f0f445 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessState.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessState.java @@ -26,16 +26,16 @@ package org.springframework.boot.availability; * @author Brian Clozel * @since 2.3.0 */ -public enum ReadinessState { - - /** - * The application is not willing to receive traffic. - */ - UNREADY, +public enum ReadinessState implements AvailabilityState { /** * The application is ready to receive traffic. */ - READY + ACCEPTING_TRAFFIC, + + /** + * The application is not willing to receive traffic. + */ + REFUSING_TRAFFIC } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessStateChangedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessStateChangedEvent.java deleted file mode 100644 index c38b15db399..00000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessStateChangedEvent.java +++ /dev/null @@ -1,58 +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.availability; - -import org.springframework.context.ApplicationEvent; - -/** - * {@link ApplicationEvent} sent when the {@link ReadinessState} of the application - * changes. - *

- * Any application component can send such events to update the state of the application. - * - * @author Brian Clozel - * @since 2.3.0 - */ -public class ReadinessStateChangedEvent extends ApplicationEvent { - - ReadinessStateChangedEvent(ReadinessState state) { - super(state); - } - - public ReadinessState getReadinessState() { - return (ReadinessState) getSource(); - } - - /** - * Create a new {@code ApplicationEvent} signaling that the {@link ReadinessState} is - * ready. - * @return the application event - */ - public static ReadinessStateChangedEvent ready() { - return new ReadinessStateChangedEvent(ReadinessState.READY); - } - - /** - * Create a new {@code ApplicationEvent} signaling that the {@link ReadinessState} is - * unready. - * @return the application event - */ - public static ReadinessStateChangedEvent unready() { - return new ReadinessStateChangedEvent(ReadinessState.UNREADY); - } - -} 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 da413c628d3..bd095161a19 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,8 +21,9 @@ 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.boot.availability.AvailabilityChangeEvent; +import org.springframework.boot.availability.LivenessState; +import org.springframework.boot.availability.ReadinessState; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; @@ -99,13 +100,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")); + AvailabilityChangeEvent.publish(context, LivenessState.CORRECT); } @Override public void running(ConfigurableApplicationContext context) { context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context)); - context.publishEvent(ReadinessStateChangedEvent.ready()); + AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC); } @Override 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 700da375292..9bc7db28e4f 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,7 +22,8 @@ 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.availability.AvailabilityChangeEvent; +import org.springframework.boot.availability.ReadinessState; import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.server.WebServer; @@ -149,7 +150,7 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli @Override protected void doClose() { - publishEvent(ReadinessStateChangedEvent.unready()); + AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC); 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 0b793071d99..f2f68447763 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,7 +37,8 @@ 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.availability.AvailabilityChangeEvent; +import org.springframework.boot.availability.ReadinessState; import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -169,7 +170,7 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon @Override protected void doClose() { - publishEvent(ReadinessStateChangedEvent.unready()); + AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC); 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 9c867979624..6353a44810d 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 @@ -33,6 +33,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatchers; import org.mockito.InOrder; import org.mockito.Mockito; @@ -46,8 +47,10 @@ 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.availability.AvailabilityChangeEvent; +import org.springframework.boot.availability.AvailabilityState; +import org.springframework.boot.availability.LivenessState; +import org.springframework.boot.availability.ReadinessState; import org.springframework.boot.context.event.ApplicationContextInitializedEvent; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationFailedEvent; @@ -108,6 +111,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.atLeastOnce; @@ -410,9 +414,10 @@ 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(argThat(isAvailabilityChangeEventWithState(LivenessState.CORRECT))); inOrder.verify(listener).onApplicationEvent(isA(ApplicationReadyEvent.class)); - inOrder.verify(listener).onApplicationEvent(isA(ReadinessStateChangedEvent.class)); + inOrder.verify(listener) + .onApplicationEvent(argThat(isAvailabilityChangeEventWithState(ReadinessState.ACCEPTING_TRAFFIC))); inOrder.verifyNoMoreInteractions(); } @@ -886,7 +891,7 @@ class SpringApplicationTests { this.context = application.run(); assertThat(events).hasAtLeastOneElementOfType(ApplicationPreparedEvent.class); assertThat(events).hasAtLeastOneElementOfType(ContextRefreshedEvent.class); - verifyTestListenerEvents(); + verifyRegisteredListenerSuccessEvents(); } @Test @@ -899,24 +904,21 @@ class SpringApplicationTests { this.context = application.run(); assertThat(events).hasAtLeastOneElementOfType(ApplicationPreparedEvent.class); assertThat(events).hasAtLeastOneElementOfType(ContextRefreshedEvent.class); - verifyTestListenerEvents(); + verifyRegisteredListenerSuccessEvents(); } @SuppressWarnings("unchecked") - private void verifyTestListenerEvents() { + private void verifyRegisteredListenerSuccessEvents() { ApplicationListener listener = this.context.getBean("testApplicationListener", ApplicationListener.class); - verifyListenerEvents(listener, ContextRefreshedEvent.class, ApplicationStartedEvent.class, - LivenessStateChangedEvent.class, ApplicationReadyEvent.class, ReadinessStateChangedEvent.class); - } - - @SuppressWarnings("unchecked") - private void verifyListenerEvents(ApplicationListener listener, - Class... eventTypes) { - for (Class eventType : eventTypes) { - verify(listener).onApplicationEvent(isA(eventType)); - } - verifyNoMoreInteractions(listener); + InOrder inOrder = Mockito.inOrder(listener); + inOrder.verify(listener).onApplicationEvent(isA(ContextRefreshedEvent.class)); + inOrder.verify(listener).onApplicationEvent(isA(ApplicationStartedEvent.class)); + inOrder.verify(listener).onApplicationEvent(argThat(isAvailabilityChangeEventWithState(LivenessState.CORRECT))); + inOrder.verify(listener).onApplicationEvent(isA(ApplicationReadyEvent.class)); + inOrder.verify(listener) + .onApplicationEvent(argThat(isAvailabilityChangeEventWithState(ReadinessState.ACCEPTING_TRAFFIC))); + inOrder.verifyNoMoreInteractions(); } @SuppressWarnings("unchecked") @@ -926,8 +928,7 @@ class SpringApplicationTests { SpringApplication application = new SpringApplication(ExampleConfig.class); application.addListeners(listener); assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(application::run); - verifyListenerEvents(listener, ApplicationStartingEvent.class, ApplicationEnvironmentPreparedEvent.class, - ApplicationContextInitializedEvent.class, ApplicationPreparedEvent.class, ApplicationFailedEvent.class); + verifyRegisteredListenerFailedFromApplicationEvents(listener); } @SuppressWarnings("unchecked") @@ -938,8 +939,17 @@ class SpringApplicationTests { application.setWebApplicationType(WebApplicationType.NONE); application.addListeners(listener); assertThatExceptionOfType(BeanCreationException.class).isThrownBy(application::run); - verifyListenerEvents(listener, ApplicationStartingEvent.class, ApplicationEnvironmentPreparedEvent.class, - ApplicationContextInitializedEvent.class, ApplicationPreparedEvent.class, ApplicationFailedEvent.class); + verifyRegisteredListenerFailedFromApplicationEvents(listener); + } + + private void verifyRegisteredListenerFailedFromApplicationEvents(ApplicationListener listener) { + InOrder inOrder = Mockito.inOrder(listener); + inOrder.verify(listener).onApplicationEvent(isA(ApplicationStartingEvent.class)); + inOrder.verify(listener).onApplicationEvent(isA(ApplicationEnvironmentPreparedEvent.class)); + inOrder.verify(listener).onApplicationEvent(isA(ApplicationContextInitializedEvent.class)); + inOrder.verify(listener).onApplicationEvent(isA(ApplicationPreparedEvent.class)); + inOrder.verify(listener).onApplicationEvent(isA(ApplicationFailedEvent.class)); + inOrder.verifyNoMoreInteractions(); } @SuppressWarnings("unchecked") @@ -949,7 +959,8 @@ class SpringApplicationTests { SpringApplication application = new SpringApplication(ExampleConfig.class); application.addInitializers((applicationContext) -> applicationContext.addApplicationListener(listener)); assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(application::run); - verifyListenerEvents(listener, ApplicationFailedEvent.class); + verify(listener).onApplicationEvent(isA(ApplicationFailedEvent.class)); + verifyNoMoreInteractions(listener); } @SuppressWarnings("unchecked") @@ -960,7 +971,17 @@ class SpringApplicationTests { application.setWebApplicationType(WebApplicationType.NONE); application.addInitializers((applicationContext) -> applicationContext.addApplicationListener(listener)); assertThatExceptionOfType(BeanCreationException.class).isThrownBy(application::run); - verifyListenerEvents(listener, ApplicationFailedEvent.class); + verify(listener).onApplicationEvent(isA(ApplicationFailedEvent.class)); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("unchecked") + private void verifyRegisteredListenerFailedFromContextEvents() { + ApplicationListener listener = this.context.getBean("testApplicationListener", + ApplicationListener.class); + InOrder inOrder = Mockito.inOrder(listener); + inOrder.verify(listener).onApplicationEvent(isA(ApplicationFailedEvent.class)); + inOrder.verifyNoMoreInteractions(); } @Test @@ -1124,6 +1145,12 @@ class SpringApplicationTests { .getBean(AtomicInteger.class)).hasValue(1); } + private ArgumentMatcher isAvailabilityChangeEventWithState( + S state) { + return (argument) -> (argument instanceof AvailabilityChangeEvent) + && ((AvailabilityChangeEvent) argument).getState().equals(state); + } + private Condition matchingPropertySource(final Class propertySourceClass, final String name) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/ApplicationAvailabilityBeanTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/ApplicationAvailabilityBeanTests.java new file mode 100644 index 00000000000..ac29c170aea --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/ApplicationAvailabilityBeanTests.java @@ -0,0 +1,110 @@ +/* + * 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.availability; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ApplicationAvailabilityBean} + * + * @author Brian Clozel + * @author Phillip Webb + */ +class ApplicationAvailabilityBeanTests { + + private AnnotationConfigApplicationContext context; + + private ApplicationAvailabilityBean availability; + + @BeforeEach + void setup() { + this.context = new AnnotationConfigApplicationContext(ApplicationAvailabilityBean.class); + this.availability = this.context.getBean(ApplicationAvailabilityBean.class); + } + + @Test + void getLivenessStateWhenNoEventHasBeenPublishedReturnsDefaultState() { + assertThat(this.availability.getLivenessState()).isEqualTo(LivenessState.BROKEN); + } + + @Test + void getLivenessStateWhenEventHasBeenPublishedReturnsPublishedState() { + AvailabilityChangeEvent.publish(this.context, LivenessState.CORRECT); + assertThat(this.availability.getLivenessState()).isEqualTo(LivenessState.CORRECT); + } + + @Test + void getReadinessStateWhenNoEventHasBeenPublishedReturnsDefaultState() { + assertThat(this.availability.getReadinessState()).isEqualTo(ReadinessState.REFUSING_TRAFFIC); + } + + @Test + void getReadinessStateWhenEventHasBeenPublishedReturnsPublishedState() { + AvailabilityChangeEvent.publish(this.context, ReadinessState.ACCEPTING_TRAFFIC); + assertThat(this.availability.getReadinessState()).isEqualTo(ReadinessState.ACCEPTING_TRAFFIC); + } + + @Test + void getStateWhenNoEventHasBeenPublishedReturnsDefaultState() { + assertThat(this.availability.getState(TestState.class)).isNull(); + assertThat(this.availability.getState(TestState.class, TestState.ONE)).isEqualTo(TestState.ONE); + } + + @Test + void getStateWhenEventHasBeenPublishedReturnsPublishedState() { + AvailabilityChangeEvent.publish(this.context, TestState.TWO); + assertThat(this.availability.getState(TestState.class)).isEqualTo(TestState.TWO); + assertThat(this.availability.getState(TestState.class, TestState.ONE)).isEqualTo(TestState.TWO); + } + + @Test + void getLastChangeEventWhenNoEventHasBeenPublishedReturnsDefaultState() { + assertThat(this.availability.getLastChangeEvent(TestState.class)).isNull(); + } + + @Test + void getLastChangeEventWhenEventHasBeenPublishedReturnsPublishedState() { + AvailabilityChangeEvent.publish(this.context, TestState.TWO); + assertThat(this.availability.getLastChangeEvent(TestState.class)).isNotNull(); + } + + enum TestState implements AvailabilityState { + + ONE { + @Override + public String test() { + return "spring"; + } + }, + + TWO { + @Override + public String test() { + return "boot"; + } + }; + + abstract String test(); + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/ApplicationAvailabilityProviderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/ApplicationAvailabilityProviderTests.java deleted file mode 100644 index b035bf56f13..00000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/ApplicationAvailabilityProviderTests.java +++ /dev/null @@ -1,52 +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.availability; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ApplicationAvailabilityProvider} - * - * @author Brian Clozel - */ -class ApplicationAvailabilityProviderTests { - - @Test - void initialStateShouldBeFailures() { - ApplicationAvailabilityProvider stateProvider = new ApplicationAvailabilityProvider(); - assertThat(stateProvider.getLivenessState()).isEqualTo(LivenessState.BROKEN); - assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.UNREADY); - } - - @Test - void updateLivenessState() { - ApplicationAvailabilityProvider stateProvider = new ApplicationAvailabilityProvider(); - LivenessState livenessState = LivenessState.LIVE; - stateProvider.onApplicationEvent(new LivenessStateChangedEvent(livenessState, "Startup complete")); - assertThat(stateProvider.getLivenessState()).isEqualTo(livenessState); - } - - @Test - void updateReadiessState() { - 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/availability/AvailabilityChangeEventTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/AvailabilityChangeEventTests.java new file mode 100644 index 00000000000..e8416b7cd0e --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/availability/AvailabilityChangeEventTests.java @@ -0,0 +1,64 @@ +/* + * 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.availability; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AvailabilityChangeEvent}. + * + * @author Phillip Webb + */ +class AvailabilityChangeEventTests { + + private Object source = new Object(); + + @Test + void createWhenStateIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new AvailabilityChangeEvent<>(this.source, null)) + .withMessage("State must not be null"); + } + + @Test + void getStateReturnsState() { + LivenessState state = LivenessState.CORRECT; + AvailabilityChangeEvent event = new AvailabilityChangeEvent<>(this.source, state); + assertThat(event.getState()).isEqualTo(state); + } + + @Test + void publishPublishesEvent() { + ApplicationContext context = mock(ApplicationContext.class); + AvailabilityState state = LivenessState.CORRECT; + AvailabilityChangeEvent.publish(context, state); + ArgumentCaptor captor = ArgumentCaptor.forClass(ApplicationEvent.class); + verify(context).publishEvent(captor.capture()); + AvailabilityChangeEvent event = (AvailabilityChangeEvent) captor.getValue(); + assertThat(event.getSource()).isEqualTo(context); + assertThat(event.getState()).isEqualTo(state); + } + +} 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 index dba0a7fce0e..ea6394d24ae 100644 --- 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 @@ -26,8 +26,7 @@ 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.boot.availability.AvailabilityChangeEvent; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.support.StaticApplicationContext; @@ -71,9 +70,9 @@ class EventPublishingRunListenerTests { checkApplicationEvents(ApplicationPreparedEvent.class); context.refresh(); this.runListener.started(context); - checkApplicationEvents(ApplicationStartedEvent.class, LivenessStateChangedEvent.class); + checkApplicationEvents(ApplicationStartedEvent.class, AvailabilityChangeEvent.class); this.runListener.running(context); - checkApplicationEvents(ApplicationReadyEvent.class, ReadinessStateChangedEvent.class); + checkApplicationEvents(ApplicationReadyEvent.class, AvailabilityChangeEvent.class); } void checkApplicationEvents(Class... eventClasses) { 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 0b036eb6abd..423702a2054 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 @@ -49,7 +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.availability.AvailabilityChangeEvent; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer; @@ -178,7 +178,7 @@ class ServletWebServerApplicationContextTests { this.context.refresh(); this.context.addApplicationListener(listener); this.context.close(); - assertThat(listener.receivedEvents()).hasSize(2).extracting("class").contains(ReadinessStateChangedEvent.class, + assertThat(listener.receivedEvents()).hasSize(2).extracting("class").contains(AvailabilityChangeEvent.class, ContextClosedEvent.class); }