From 9242def4c0753278599614d22e07c076f5e246ec Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 4 Sep 2017 14:26:31 +0100 Subject: [PATCH] Improve structure and JSON serialization of beans endpoint's response Closes gh-10156 --- .../endpoint/EndpointAutoConfiguration.java | 13 +- .../boot/actuate/endpoint/BeansEndpoint.java | 187 +++++++++++++----- .../actuate/endpoint/BeansEndpointTests.java | 133 ++++++++----- .../mvc/MvcEndpointCorsIntegrationTests.java | 6 +- .../SampleActuatorApplicationTests.java | 9 +- 5 files changed, 239 insertions(+), 109 deletions(-) diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java index f40f0d5b5c..31261b83da 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java @@ -64,6 +64,7 @@ import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.endpoint.Endpoint; import org.springframework.boot.logging.LoggingSystem; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -101,8 +102,9 @@ public class EndpointAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnEnabledEndpoint - public BeansEndpoint beansEndpoint() { - return new BeansEndpoint(); + public BeansEndpoint beansEndpoint( + ConfigurableApplicationContext applicationContext) { + return new BeansEndpoint(applicationContext); } @Bean @@ -187,9 +189,10 @@ public class EndpointAutoConfiguration { HealthEndpointConfiguration(ObjectProvider healthAggregator, Supplier> healthIndicatorsSupplier) { - this.healthIndicator = new CompositeHealthIndicatorFactory().createHealthIndicator( - healthAggregator.getIfAvailable(OrderedHealthAggregator::new), - healthIndicatorsSupplier.get()); + this.healthIndicator = new CompositeHealthIndicatorFactory() + .createHealthIndicator( + healthAggregator.getIfAvailable(OrderedHealthAggregator::new), + healthIndicatorsSupplier.get()); } @Bean diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/BeansEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/BeansEndpoint.java index 54b7e79939..760dd963c2 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/BeansEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/BeansEndpoint.java @@ -16,83 +16,174 @@ package org.springframework.boot.actuate.endpoint; -import java.util.LinkedHashSet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.Map; -import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.endpoint.Endpoint; import org.springframework.boot.endpoint.ReadOperation; -import org.springframework.boot.json.JsonParser; -import org.springframework.boot.json.JsonParserFactory; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.support.LiveBeansView; -import org.springframework.core.env.Environment; -import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** - * Exposes JSON view of Spring beans. If the {@link Environment} contains a key setting - * the {@link LiveBeansView#MBEAN_DOMAIN_PROPERTY_NAME} then all application contexts in - * the JVM will be shown (and the corresponding MBeans will be registered per the standard - * behavior of LiveBeansView). Otherwise only the current application context hierarchy. + * {@link Endpoint} to expose details of an application's bean, grouped by application + * context. * * @author Dave Syer * @author Andy Wilkinson */ @Endpoint(id = "beans") -public class BeansEndpoint implements ApplicationContextAware { +public class BeansEndpoint { - private final HierarchyAwareLiveBeansView liveBeansView = new HierarchyAwareLiveBeansView(); + private final ConfigurableApplicationContext context; - private final JsonParser parser = JsonParserFactory.getJsonParser(); - - @Override - public void setApplicationContext(ApplicationContext context) throws BeansException { - if (context.getEnvironment() - .getProperty(LiveBeansView.MBEAN_DOMAIN_PROPERTY_NAME) == null) { - this.liveBeansView.setLeafContext(context); - } + /** + * Creates a new {@code BeansEndpoint} that will describe the beans in the given + * {@code context} and all of its ancestors. + * + * @param context the application context + * @see ConfigurableApplicationContext#getParent() + */ + public BeansEndpoint(ConfigurableApplicationContext context) { + this.context = context; } @ReadOperation - public List beans() { - return this.parser.parseList(this.liveBeansView.getSnapshotAsJson()); + public Map beans() { + List contexts = new ArrayList<>(); + ConfigurableApplicationContext current = this.context; + while (current != null) { + contexts.add(ApplicationContextDescriptor.describing(current)); + current = getConfigurableParent(current); + } + return Collections.singletonMap("contexts", contexts); } - private static class HierarchyAwareLiveBeansView extends LiveBeansView { + private ConfigurableApplicationContext getConfigurableParent( + ConfigurableApplicationContext context) { + ApplicationContext parent = context.getParent(); + if (parent instanceof ConfigurableApplicationContext) { + return (ConfigurableApplicationContext) parent; + } + return null; + } - private ConfigurableApplicationContext leafContext; + /** + * A description of an application context, primarily intended for serialization to + * JSON. + */ + static final class ApplicationContextDescriptor { - private void setLeafContext(ApplicationContext leafContext) { - this.leafContext = asConfigurableContext(leafContext); + private final String id; + + private final String parentId; + + private final Map beans; + + private ApplicationContextDescriptor(String id, String parentId, + Map beans) { + this.id = id; + this.parentId = parentId; + this.beans = beans; } - @Override - public String getSnapshotAsJson() { - if (this.leafContext == null) { - return super.getSnapshotAsJson(); + public String getId() { + return this.id; + } + + public String getParentId() { + return this.parentId; + } + + public Map getBeans() { + return this.beans; + } + + private static ApplicationContextDescriptor describing( + ConfigurableApplicationContext context) { + ApplicationContext parent = context.getParent(); + return new ApplicationContextDescriptor(context.getId(), + parent == null ? null : parent.getId(), + describeBeans(context.getBeanFactory())); + } + + private static Map describeBeans( + ConfigurableListableBeanFactory beanFactory) { + Map beans = new HashMap<>(); + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition definition = beanFactory.getBeanDefinition(beanName); + if (isBeanEligible(beanName, definition, beanFactory)) { + beans.put(beanName, describeBean(beanName, definition, beanFactory)); + } } - return generateJson(getContextHierarchy()); + return beans; } - private ConfigurableApplicationContext asConfigurableContext( - ApplicationContext applicationContext) { - Assert.isTrue(applicationContext instanceof ConfigurableApplicationContext, - "'" + applicationContext - + "' does not implement ConfigurableApplicationContext"); - return (ConfigurableApplicationContext) applicationContext; + private static BeanDescriptor describeBean(String name, BeanDefinition definition, + ConfigurableListableBeanFactory factory) { + return new BeanDescriptor(factory.getAliases(name), definition.getScope(), + factory.getType(name), definition.getResourceDescription(), + factory.getDependenciesForBean(name)); } - private Set getContextHierarchy() { - Set contexts = new LinkedHashSet<>(); - ApplicationContext context = this.leafContext; - while (context != null) { - contexts.add(asConfigurableContext(context)); - context = context.getParent(); - } - return contexts; + private static boolean isBeanEligible(String beanName, BeanDefinition bd, + ConfigurableBeanFactory bf) { + return (bd.getRole() != BeanDefinition.ROLE_INFRASTRUCTURE + && (!bd.isLazyInit() || bf.containsSingleton(beanName))); + } + + } + + /** + * A description of a bean in an application context, primarily intended for + * serialization to JSON. + */ + static final class BeanDescriptor { + + private final String[] aliases; + + private final String scope; + + private final Class type; + + private final String resource; + + private final String[] dependencies; + + private BeanDescriptor(String[] aliases, String scope, Class type, + String resource, String[] dependencies) { + this.aliases = aliases; + this.scope = StringUtils.hasText(scope) ? scope + : BeanDefinition.SCOPE_SINGLETON; + this.type = type; + this.resource = resource; + this.dependencies = dependencies; + } + + public String[] getAliases() { + return this.aliases; + } + + public String getScope() { + return this.scope; + } + + public Class getType() { + return this.type; + } + + public String getResource() { + return this.resource; + } + + public String[] getDependencies() { + return this.dependencies; } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointTests.java index 2d9145432b..7d23764564 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointTests.java @@ -18,13 +18,20 @@ package org.springframework.boot.actuate.endpoint; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import org.assertj.core.api.Condition; import org.junit.Test; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.actuate.endpoint.BeansEndpoint.ApplicationContextDescriptor; +import org.springframework.boot.actuate.endpoint.BeansEndpoint.BeanDescriptor; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import static org.assertj.core.api.Assertions.assertThat; @@ -36,17 +43,64 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class BeansEndpointTests { + @SuppressWarnings("unchecked") @Test - public void beansAreFound() throws Exception { + public void beansAreFound() { ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withUserConfiguration(EndpointConfiguration.class); contextRunner.run((context) -> { - List result = context.getBean(BeansEndpoint.class).beans(); - assertThat(result).hasSize(1); - assertThat(result.get(0)).isInstanceOf(Map.class); + Map result = context.getBean(BeansEndpoint.class).beans(); + List contexts = (List) result + .get("contexts"); + assertThat(contexts).hasSize(1); + ApplicationContextDescriptor contextDescriptor = contexts.get(0); + assertThat(contextDescriptor.getParentId()).isNull(); + assertThat(contextDescriptor.getId()).isEqualTo(context.getId()); + Map beans = contextDescriptor.getBeans(); + assertThat(beans.size()) + .isLessThanOrEqualTo(context.getBeanDefinitionCount()); + assertThat(contexts.get(0).getBeans()).containsKey("endpoint"); }); } + @SuppressWarnings("unchecked") + @Test + public void infrastructureBeansAreOmitted() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(EndpointConfiguration.class); + contextRunner.run((context) -> { + ConfigurableListableBeanFactory factory = (ConfigurableListableBeanFactory) context + .getAutowireCapableBeanFactory(); + List infrastructureBeans = Stream.of(context.getBeanDefinitionNames()) + .filter((name) -> BeanDefinition.ROLE_INFRASTRUCTURE == factory + .getBeanDefinition(name).getRole()) + .collect(Collectors.toList()); + Map result = context.getBean(BeansEndpoint.class).beans(); + List contexts = (List) result + .get("contexts"); + Map beans = contexts.get(0).getBeans(); + for (String infrastructureBean : infrastructureBeans) { + assertThat(beans).doesNotContainKey(infrastructureBean); + } + }); + } + + @SuppressWarnings("unchecked") + @Test + public void lazyBeansAreOmitted() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(EndpointConfiguration.class, + LazyBeanConfiguration.class); + contextRunner.run((context) -> { + Map result = context.getBean(BeansEndpoint.class).beans(); + List contexts = (List) result + .get("contexts"); + assertThat(context).hasBean("lazyBean"); + assertThat(contexts.get(0).getBeans()).doesNotContainKey("lazyBean"); + }); + } + + @SuppressWarnings("unchecked") @Test public void beansInParentContextAreFound() { ApplicationContextRunner parentRunner = new ApplicationContextRunner() @@ -56,14 +110,17 @@ public class BeansEndpointTests { .withUserConfiguration(EndpointConfiguration.class).withParent(parent) .run(child -> { BeansEndpoint endpoint = child.getBean(BeansEndpoint.class); - List contexts = endpoint.beans(); + Map result = endpoint.beans(); + List contexts = (List) result + .get("contexts"); assertThat(contexts).hasSize(2); - assertThat(contexts.get(1)).has(beanNamed("bean")); - assertThat(contexts.get(0)).has(beanNamed("endpoint")); + assertThat(contexts.get(1).getBeans()).containsKey("bean"); + assertThat(contexts.get(0).getBeans()).containsKey("endpoint"); }); }); } + @SuppressWarnings("unchecked") @Test public void beansInChildContextAreNotFound() { ApplicationContextRunner parentRunner = new ApplicationContextRunner() @@ -72,57 +129,22 @@ public class BeansEndpointTests { new ApplicationContextRunner().withUserConfiguration(BeanConfiguration.class) .withParent(parent).run(child -> { BeansEndpoint endpoint = child.getBean(BeansEndpoint.class); - List contexts = endpoint.beans(); + Map result = endpoint.beans(); + List contexts = (List) result + .get("contexts"); assertThat(contexts).hasSize(1); - assertThat(contexts.get(0)).has(beanNamed("endpoint")); - assertThat(contexts.get(0)).doesNotHave(beanNamed("bean")); + assertThat(contexts.get(0).getBeans()).containsKey("endpoint"); + assertThat(contexts.get(0).getBeans()).doesNotContainKey("bean"); }); }); } - private ContextHasBeanCondition beanNamed(String beanName) { - return new ContextHasBeanCondition(beanName); - } - - private static final class ContextHasBeanCondition extends Condition { - - private final String beanName; - - private ContextHasBeanCondition(String beanName) { - super("Bean named '" + beanName + "'"); - this.beanName = beanName; - } - - @Override - @SuppressWarnings("unchecked") - public boolean matches(Object context) { - if (!(context instanceof Map)) { - return false; - } - List beans = (List) ((Map) context) - .get("beans"); - if (beans == null) { - return false; - } - for (Object bean : beans) { - if (!(bean instanceof Map)) { - return false; - } - if (this.beanName.equals(((Map) bean).get("bean"))) { - return true; - } - } - return false; - } - - } - @Configuration public static class EndpointConfiguration { @Bean - public BeansEndpoint endpoint() { - return new BeansEndpoint(); + public BeansEndpoint endpoint(ConfigurableApplicationContext context) { + return new BeansEndpoint(context); } } @@ -137,4 +159,15 @@ public class BeansEndpointTests { } + @Configuration + static class LazyBeanConfiguration { + + @Lazy + @Bean + public String lazyBean() { + return "lazyBean"; + } + + } + } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java index 29f3623e1c..a53c408d9c 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java @@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportM import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; @@ -197,8 +198,9 @@ public class MvcEndpointCorsIntegrationTests { static class EndpointConfiguration { @Bean - public BeansEndpoint beansEndpoint() { - return new BeansEndpoint(); + public BeansEndpoint beansEndpoint( + ConfigurableApplicationContext applicationContext) { + return new BeansEndpoint(applicationContext); } } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java index 5b71cb3d29..ec6cb816fe 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/SampleActuatorApplicationTests.java @@ -213,13 +213,14 @@ public class SampleActuatorApplicationTests { @SuppressWarnings("unchecked") public void testBeans() throws Exception { @SuppressWarnings("rawtypes") - ResponseEntity entity = this.restTemplate + ResponseEntity entity = this.restTemplate .withBasicAuth("user", getPassword()) - .getForEntity("/application/beans", List.class); + .getForEntity("/application/beans", Map.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).hasSize(1); - Map body = (Map) entity.getBody().get(0); - assertThat(((String) body.get("context"))).startsWith("application"); + Map body = (Map) ((List) entity.getBody() + .get("contexts")).get(0); + assertThat(((String) body.get("id"))).startsWith("application"); } @Test