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
This commit is contained in:
Phillip Webb 2020-04-09 16:04:25 -07:00
parent 473d4fd73d
commit bb79c847b2
26 changed files with 561 additions and 369 deletions

View File

@ -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

View File

@ -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));

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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)));
}
}

View File

@ -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.
* <p>
* 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 <S> 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 extends AvailabilityState> S getState(Class<S> stateType, S defaultState);
/**
* Return {@link AvailabilityState} information for the application.
* @param <S> 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 extends AvailabilityState> S getState(Class<S> stateType);
/**
* Return the last {@link AvailabilityChangeEvent} received for a given state type.
* @param <S> 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
*/
<S extends AvailabilityState> AvailabilityChangeEvent<S> getLastChangeEvent(Class<S> stateType);
}

View File

@ -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<AvailabilityChangeEvent<?>> {
private final Map<Class<? extends AvailabilityState>, AvailabilityChangeEvent<?>> events = new HashMap<>();
@Override
public <S extends AvailabilityState> S getState(Class<S> 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 extends AvailabilityState> S getState(Class<S> stateType) {
AvailabilityChangeEvent<S> event = getLastChangeEvent(stateType);
return (event != null) ? event.getState() : null;
}
@Override
@SuppressWarnings("unchecked")
public <S extends AvailabilityState> AvailabilityChangeEvent<S> getLastChangeEvent(Class<S> stateType) {
return (AvailabilityChangeEvent<S>) this.events.get(stateType);
}
@Override
public void onApplicationEvent(AvailabilityChangeEvent<?> event) {
Class<? extends AvailabilityState> stateType = getStateType(event.getState());
this.events.put(stateType, event);
}
@SuppressWarnings("unchecked")
private Class<? extends AvailabilityState> getStateType(AvailabilityState state) {
if (state instanceof Enum) {
return (Class<? extends AvailabilityState>) ((Enum<?>) state).getDeclaringClass();
}
return state.getClass();
}
}

View File

@ -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.
* <p>
* 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<ApplicationEvent> {
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();
}
}
}

View File

@ -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.
* <p>
* Any application component can send such events to update the state of the application.
*
* @param <S> the availability state type
* @author Brian Clozel
* @author Phillip Webb
* @since 2.3.0
*/
public class AvailabilityChangeEvent<S extends AvailabilityState> 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 <S> the availability state type
* @param context the context used to publish the event
* @param state the changed availability state
*/
public static <S extends AvailabilityState> 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 <S> 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 <S extends AvailabilityState> void publish(ApplicationEventPublisher publisher, Object source,
S state) {
Assert.notNull(publisher, "Publisher must not be null");
publisher.publishEvent(new AvailabilityChangeEvent<>(source, state));
}
}

View File

@ -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 {
}

View File

@ -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

View File

@ -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.
* <p>
* 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());
}
}

View File

@ -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
}

View File

@ -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.
* <p>
* 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);
}
}

View File

@ -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

View File

@ -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();

View File

@ -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();

View File

@ -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<ApplicationEvent> 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<ApplicationEvent> listener,
Class<? extends ApplicationEvent>... eventTypes) {
for (Class<? extends ApplicationEvent> 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<ApplicationEvent> 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<ApplicationEvent> 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 <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
S state) {
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)
&& ((AvailabilityChangeEvent<?>) argument).getState().equals(state);
}
private Condition<ConfigurableEnvironment> matchingPropertySource(final Class<?> propertySourceClass,
final String name) {

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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<LivenessState> 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<ApplicationEvent> 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);
}
}

View File

@ -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) {

View File

@ -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);
}