From ae5ae72889926d1eabe1ca492e0d77479d9c659e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 30 Oct 2019 12:18:42 -0700 Subject: [PATCH] Add and adapt reactive health contributors Update `HealthEndpointConfiguration` to also include adapted reactive health contributors when project reactor is on the classpath. Prior to this commit, reactive contributors were only exposed in WebFlux applications. This was a regression from Spring Boot 2.1 that we didn't catch because all our own reactive contributors all have non-reactive equivalents. Closes gh-18805 --- .../health/HealthEndpointConfiguration.java | 95 ++++++++++++++++++- .../HealthEndpointAutoConfigurationTests.java | 11 +++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java index ed7e854fa1f..54b074dce32 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java @@ -16,13 +16,23 @@ package org.springframework.boot.actuate.autoconfigure.health; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.boot.actuate.health.CompositeHealthContributor; +import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor; +import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthContributor; import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpointGroups; +import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; +import org.springframework.boot.actuate.health.NamedContributor; +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.actuate.health.ReactiveHealthIndicator; import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper; import org.springframework.boot.actuate.health.SimpleStatusAggregator; import org.springframework.boot.actuate.health.StatusAggregator; @@ -30,6 +40,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.util.ClassUtils; /** * Configuration for {@link HealthEndpoint} infrastructure beans. @@ -61,8 +72,13 @@ class HealthEndpointConfiguration { @Bean @ConditionalOnMissingBean - HealthContributorRegistry healthContributorRegistry(Map healthContributors, + HealthContributorRegistry healthContributorRegistry(ApplicationContext applicationContext, HealthEndpointGroups groups) { + Map healthContributors = new LinkedHashMap<>( + applicationContext.getBeansOfType(HealthContributor.class)); + if (ClassUtils.isPresent("reactor.core.publisher.Flux", applicationContext.getClassLoader())) { + healthContributors.putAll(new AdaptedReactiveHealthContributors(applicationContext).get()); + } return new AutoConfiguredHealthContributorRegistry(healthContributors, groups.getNames()); } @@ -72,4 +88,81 @@ class HealthEndpointConfiguration { return new HealthEndpoint(registry, groups); } + /** + * Adapter to expose {@link ReactiveHealthContributor} beans as + * {@link HealthContributor} instances. + */ + private static class AdaptedReactiveHealthContributors { + + private final Map adapted; + + AdaptedReactiveHealthContributors(ApplicationContext applicationContext) { + Map adapted = new LinkedHashMap<>(); + applicationContext.getBeansOfType(ReactiveHealthContributor.class) + .forEach((name, contributor) -> adapted.put(name, adapt(contributor))); + this.adapted = Collections.unmodifiableMap(adapted); + } + + private HealthContributor adapt(ReactiveHealthContributor contributor) { + if (contributor instanceof ReactiveHealthIndicator) { + return adapt((ReactiveHealthIndicator) contributor); + } + if (contributor instanceof CompositeReactiveHealthContributor) { + return adapt((CompositeReactiveHealthContributor) contributor); + } + throw new IllegalStateException("Unsupported ReactiveHealthContributor type " + contributor.getClass()); + } + + private HealthIndicator adapt(ReactiveHealthIndicator indicator) { + return new HealthIndicator() { + + @Override + public Health getHealth(boolean includeDetails) { + return indicator.getHealth(includeDetails).block(); + } + + @Override + public Health health() { + return indicator.health().block(); + } + + }; + } + + private CompositeHealthContributor adapt(CompositeReactiveHealthContributor composite) { + return new CompositeHealthContributor() { + + @Override + public Iterator> iterator() { + Iterator> iterator = composite.iterator(); + return new Iterator>() { + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public NamedContributor next() { + NamedContributor next = iterator.next(); + return NamedContributor.of(next.getName(), adapt(next.getContributor())); + } + + }; + } + + @Override + public HealthContributor getContributor(String name) { + return adapt(composite.getContributor(name)); + } + + }; + } + + Map get() { + return this.adapted; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java index a56ce593454..092d6cdee09 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.endpoint.SecurityContext; @@ -178,6 +179,16 @@ class HealthEndpointAutoConfigurationTests { @Test void runCreatesHealthContributorRegistryContainingHealthBeans() { this.contextRunner.run((context) -> { + HealthContributorRegistry registry = context.getBean(HealthContributorRegistry.class); + Object[] names = registry.stream().map(NamedContributor::getName).toArray(); + assertThat(names).containsExactlyInAnyOrder("simple", "additional", "ping", "reactive"); + }); + } + + @Test + void runWhenNoReactorCreatesHealthContributorRegistryContainingHealthBeans() { + ClassLoader classLoader = new FilteredClassLoader(Mono.class, Flux.class); + this.contextRunner.withClassLoader(classLoader).run((context) -> { HealthContributorRegistry registry = context.getBean(HealthContributorRegistry.class); Object[] names = registry.stream().map(NamedContributor::getName).toArray(); assertThat(names).containsExactlyInAnyOrder("simple", "additional", "ping");