Commit 9242def4 authored by Andy Wilkinson's avatar Andy Wilkinson

Improve structure and JSON serialization of beans endpoint's response

Closes gh-10156
parent 9ffbfb0d
...@@ -64,6 +64,7 @@ import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; ...@@ -64,6 +64,7 @@ import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
import org.springframework.boot.endpoint.Endpoint; import org.springframework.boot.endpoint.Endpoint;
import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
...@@ -101,8 +102,9 @@ public class EndpointAutoConfiguration { ...@@ -101,8 +102,9 @@ public class EndpointAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnEnabledEndpoint @ConditionalOnEnabledEndpoint
public BeansEndpoint beansEndpoint() { public BeansEndpoint beansEndpoint(
return new BeansEndpoint(); ConfigurableApplicationContext applicationContext) {
return new BeansEndpoint(applicationContext);
} }
@Bean @Bean
...@@ -187,9 +189,10 @@ public class EndpointAutoConfiguration { ...@@ -187,9 +189,10 @@ public class EndpointAutoConfiguration {
HealthEndpointConfiguration(ObjectProvider<HealthAggregator> healthAggregator, HealthEndpointConfiguration(ObjectProvider<HealthAggregator> healthAggregator,
Supplier<Map<String, HealthIndicator>> healthIndicatorsSupplier) { Supplier<Map<String, HealthIndicator>> healthIndicatorsSupplier) {
this.healthIndicator = new CompositeHealthIndicatorFactory().createHealthIndicator( this.healthIndicator = new CompositeHealthIndicatorFactory()
healthAggregator.getIfAvailable(OrderedHealthAggregator::new), .createHealthIndicator(
healthIndicatorsSupplier.get()); healthAggregator.getIfAvailable(OrderedHealthAggregator::new),
healthIndicatorsSupplier.get());
} }
@Bean @Bean
......
...@@ -16,83 +16,174 @@ ...@@ -16,83 +16,174 @@
package org.springframework.boot.actuate.endpoint; 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.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.Endpoint;
import org.springframework.boot.endpoint.ReadOperation; 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.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.LiveBeansView; import org.springframework.util.StringUtils;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
/** /**
* Exposes JSON view of Spring beans. If the {@link Environment} contains a key setting * {@link Endpoint} to expose details of an application's bean, grouped by application
* the {@link LiveBeansView#MBEAN_DOMAIN_PROPERTY_NAME} then all application contexts in * context.
* 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.
* *
* @author Dave Syer * @author Dave Syer
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
@Endpoint(id = "beans") @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(); /**
* 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;
}
@Override @ReadOperation
public void setApplicationContext(ApplicationContext context) throws BeansException { public Map<String, Object> beans() {
if (context.getEnvironment() List<ApplicationContextDescriptor> contexts = new ArrayList<>();
.getProperty(LiveBeansView.MBEAN_DOMAIN_PROPERTY_NAME) == null) { ConfigurableApplicationContext current = this.context;
this.liveBeansView.setLeafContext(context); while (current != null) {
contexts.add(ApplicationContextDescriptor.describing(current));
current = getConfigurableParent(current);
} }
return Collections.singletonMap("contexts", contexts);
} }
@ReadOperation private ConfigurableApplicationContext getConfigurableParent(
public List<Object> beans() { ConfigurableApplicationContext context) {
return this.parser.parseList(this.liveBeansView.getSnapshotAsJson()); ApplicationContext parent = context.getParent();
if (parent instanceof ConfigurableApplicationContext) {
return (ConfigurableApplicationContext) parent;
}
return null;
} }
private static class HierarchyAwareLiveBeansView extends LiveBeansView { /**
* A description of an application context, primarily intended for serialization to
* JSON.
*/
static final class ApplicationContextDescriptor {
private final String id;
private final String parentId;
private ConfigurableApplicationContext leafContext; private final Map<String, BeanDescriptor> beans;
private void setLeafContext(ApplicationContext leafContext) { private ApplicationContextDescriptor(String id, String parentId,
this.leafContext = asConfigurableContext(leafContext); Map<String, BeanDescriptor> beans) {
this.id = id;
this.parentId = parentId;
this.beans = beans;
} }
@Override public String getId() {
public String getSnapshotAsJson() { return this.id;
if (this.leafContext == null) {
return super.getSnapshotAsJson();
}
return generateJson(getContextHierarchy());
} }
private ConfigurableApplicationContext asConfigurableContext( public String getParentId() {
ApplicationContext applicationContext) { return this.parentId;
Assert.isTrue(applicationContext instanceof ConfigurableApplicationContext,
"'" + applicationContext
+ "' does not implement ConfigurableApplicationContext");
return (ConfigurableApplicationContext) applicationContext;
} }
private Set<ConfigurableApplicationContext> getContextHierarchy() { public Map<String, BeanDescriptor> getBeans() {
Set<ConfigurableApplicationContext> contexts = new LinkedHashSet<>(); return this.beans;
ApplicationContext context = this.leafContext; }
while (context != null) {
contexts.add(asConfigurableContext(context)); private static ApplicationContextDescriptor describing(
context = context.getParent(); ConfigurableApplicationContext context) {
ApplicationContext parent = context.getParent();
return new ApplicationContextDescriptor(context.getId(),
parent == null ? null : parent.getId(),
describeBeans(context.getBeanFactory()));
}
private static Map<String, BeanDescriptor> describeBeans(
ConfigurableListableBeanFactory beanFactory) {
Map<String, BeanDescriptor> 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 contexts; return beans;
}
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 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;
} }
} }
......
...@@ -18,13 +18,20 @@ package org.springframework.boot.actuate.endpoint; ...@@ -18,13 +18,20 @@ package org.springframework.boot.actuate.endpoint;
import java.util.List; import java.util.List;
import java.util.Map; 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.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.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -36,17 +43,64 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -36,17 +43,64 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
public class BeansEndpointTests { public class BeansEndpointTests {
@SuppressWarnings("unchecked")
@Test @Test
public void beansAreFound() throws Exception { public void beansAreFound() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner() ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withUserConfiguration(EndpointConfiguration.class); .withUserConfiguration(EndpointConfiguration.class);
contextRunner.run((context) -> { contextRunner.run((context) -> {
List<Object> result = context.getBean(BeansEndpoint.class).beans(); Map<String, Object> result = context.getBean(BeansEndpoint.class).beans();
assertThat(result).hasSize(1); List<ApplicationContextDescriptor> contexts = (List<ApplicationContextDescriptor>) result
assertThat(result.get(0)).isInstanceOf(Map.class); .get("contexts");
assertThat(contexts).hasSize(1);
ApplicationContextDescriptor contextDescriptor = contexts.get(0);
assertThat(contextDescriptor.getParentId()).isNull();
assertThat(contextDescriptor.getId()).isEqualTo(context.getId());
Map<String, BeanDescriptor> 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<String> infrastructureBeans = Stream.of(context.getBeanDefinitionNames())
.filter((name) -> BeanDefinition.ROLE_INFRASTRUCTURE == factory
.getBeanDefinition(name).getRole())
.collect(Collectors.toList());
Map<String, Object> result = context.getBean(BeansEndpoint.class).beans();
List<ApplicationContextDescriptor> contexts = (List<ApplicationContextDescriptor>) result
.get("contexts");
Map<String, BeanDescriptor> 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<String, Object> result = context.getBean(BeansEndpoint.class).beans();
List<ApplicationContextDescriptor> contexts = (List<ApplicationContextDescriptor>) result
.get("contexts");
assertThat(context).hasBean("lazyBean");
assertThat(contexts.get(0).getBeans()).doesNotContainKey("lazyBean");
});
}
@SuppressWarnings("unchecked")
@Test @Test
public void beansInParentContextAreFound() { public void beansInParentContextAreFound() {
ApplicationContextRunner parentRunner = new ApplicationContextRunner() ApplicationContextRunner parentRunner = new ApplicationContextRunner()
...@@ -56,14 +110,17 @@ public class BeansEndpointTests { ...@@ -56,14 +110,17 @@ public class BeansEndpointTests {
.withUserConfiguration(EndpointConfiguration.class).withParent(parent) .withUserConfiguration(EndpointConfiguration.class).withParent(parent)
.run(child -> { .run(child -> {
BeansEndpoint endpoint = child.getBean(BeansEndpoint.class); BeansEndpoint endpoint = child.getBean(BeansEndpoint.class);
List<Object> contexts = endpoint.beans(); Map<String, Object> result = endpoint.beans();
List<ApplicationContextDescriptor> contexts = (List<ApplicationContextDescriptor>) result
.get("contexts");
assertThat(contexts).hasSize(2); assertThat(contexts).hasSize(2);
assertThat(contexts.get(1)).has(beanNamed("bean")); assertThat(contexts.get(1).getBeans()).containsKey("bean");
assertThat(contexts.get(0)).has(beanNamed("endpoint")); assertThat(contexts.get(0).getBeans()).containsKey("endpoint");
}); });
}); });
} }
@SuppressWarnings("unchecked")
@Test @Test
public void beansInChildContextAreNotFound() { public void beansInChildContextAreNotFound() {
ApplicationContextRunner parentRunner = new ApplicationContextRunner() ApplicationContextRunner parentRunner = new ApplicationContextRunner()
...@@ -72,67 +129,43 @@ public class BeansEndpointTests { ...@@ -72,67 +129,43 @@ public class BeansEndpointTests {
new ApplicationContextRunner().withUserConfiguration(BeanConfiguration.class) new ApplicationContextRunner().withUserConfiguration(BeanConfiguration.class)
.withParent(parent).run(child -> { .withParent(parent).run(child -> {
BeansEndpoint endpoint = child.getBean(BeansEndpoint.class); BeansEndpoint endpoint = child.getBean(BeansEndpoint.class);
List<Object> contexts = endpoint.beans(); Map<String, Object> result = endpoint.beans();
List<ApplicationContextDescriptor> contexts = (List<ApplicationContextDescriptor>) result
.get("contexts");
assertThat(contexts).hasSize(1); assertThat(contexts).hasSize(1);
assertThat(contexts.get(0)).has(beanNamed("endpoint")); assertThat(contexts.get(0).getBeans()).containsKey("endpoint");
assertThat(contexts.get(0)).doesNotHave(beanNamed("bean")); assertThat(contexts.get(0).getBeans()).doesNotContainKey("bean");
}); });
}); });
} }
private ContextHasBeanCondition beanNamed(String beanName) { @Configuration
return new ContextHasBeanCondition(beanName); public static class EndpointConfiguration {
}
private static final class ContextHasBeanCondition extends Condition<Object> {
private final String beanName;
private ContextHasBeanCondition(String beanName) {
super("Bean named '" + beanName + "'");
this.beanName = beanName;
}
@Override @Bean
@SuppressWarnings("unchecked") public BeansEndpoint endpoint(ConfigurableApplicationContext context) {
public boolean matches(Object context) { return new BeansEndpoint(context);
if (!(context instanceof Map)) {
return false;
}
List<Object> beans = (List<Object>) ((Map<String, Object>) context)
.get("beans");
if (beans == null) {
return false;
}
for (Object bean : beans) {
if (!(bean instanceof Map)) {
return false;
}
if (this.beanName.equals(((Map<String, Object>) bean).get("bean"))) {
return true;
}
}
return false;
} }
} }
@Configuration @Configuration
public static class EndpointConfiguration { static class BeanConfiguration {
@Bean @Bean
public BeansEndpoint endpoint() { public String bean() {
return new BeansEndpoint(); return "bean";
} }
} }
@Configuration @Configuration
static class BeanConfiguration { static class LazyBeanConfiguration {
@Lazy
@Bean @Bean
public String bean() { public String lazyBean() {
return "bean"; return "lazyBean";
} }
} }
......
...@@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportM ...@@ -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.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
...@@ -197,8 +198,9 @@ public class MvcEndpointCorsIntegrationTests { ...@@ -197,8 +198,9 @@ public class MvcEndpointCorsIntegrationTests {
static class EndpointConfiguration { static class EndpointConfiguration {
@Bean @Bean
public BeansEndpoint beansEndpoint() { public BeansEndpoint beansEndpoint(
return new BeansEndpoint(); ConfigurableApplicationContext applicationContext) {
return new BeansEndpoint(applicationContext);
} }
} }
......
...@@ -213,13 +213,14 @@ public class SampleActuatorApplicationTests { ...@@ -213,13 +213,14 @@ public class SampleActuatorApplicationTests {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void testBeans() throws Exception { public void testBeans() throws Exception {
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
ResponseEntity<List> entity = this.restTemplate ResponseEntity<Map> entity = this.restTemplate
.withBasicAuth("user", getPassword()) .withBasicAuth("user", getPassword())
.getForEntity("/application/beans", List.class); .getForEntity("/application/beans", Map.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).hasSize(1); assertThat(entity.getBody()).hasSize(1);
Map<String, Object> body = (Map<String, Object>) entity.getBody().get(0); Map<String, Object> body = (Map<String, Object>) ((List<?>) entity.getBody()
assertThat(((String) body.get("context"))).startsWith("application"); .get("contexts")).get(0);
assertThat(((String) body.get("id"))).startsWith("application");
} }
@Test @Test
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment