Improve structure and JSON serialization of beans endpoint's response

Closes gh-10156
This commit is contained in:
Andy Wilkinson 2017-09-04 14:26:31 +01:00
parent 9ffbfb0d80
commit 9242def4c0
5 changed files with 239 additions and 109 deletions

View File

@ -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> healthAggregator,
Supplier<Map<String, HealthIndicator>> healthIndicatorsSupplier) {
this.healthIndicator = new CompositeHealthIndicatorFactory().createHealthIndicator(
healthAggregator.getIfAvailable(OrderedHealthAggregator::new),
healthIndicatorsSupplier.get());
this.healthIndicator = new CompositeHealthIndicatorFactory()
.createHealthIndicator(
healthAggregator.getIfAvailable(OrderedHealthAggregator::new),
healthIndicatorsSupplier.get());
}
@Bean

View File

@ -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<Object> beans() {
return this.parser.parseList(this.liveBeansView.getSnapshotAsJson());
public Map<String, Object> beans() {
List<ApplicationContextDescriptor> 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<String, BeanDescriptor> beans;
private ApplicationContextDescriptor(String id, String parentId,
Map<String, BeanDescriptor> 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<String, BeanDescriptor> 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<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 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<ConfigurableApplicationContext> getContextHierarchy() {
Set<ConfigurableApplicationContext> 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;
}
}

View File

@ -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<Object> result = context.getBean(BeansEndpoint.class).beans();
assertThat(result).hasSize(1);
assertThat(result.get(0)).isInstanceOf(Map.class);
Map<String, Object> result = context.getBean(BeansEndpoint.class).beans();
List<ApplicationContextDescriptor> contexts = (List<ApplicationContextDescriptor>) result
.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
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<Object> contexts = endpoint.beans();
Map<String, Object> result = endpoint.beans();
List<ApplicationContextDescriptor> contexts = (List<ApplicationContextDescriptor>) 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<Object> contexts = endpoint.beans();
Map<String, Object> result = endpoint.beans();
List<ApplicationContextDescriptor> contexts = (List<ApplicationContextDescriptor>) 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<Object> {
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<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
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";
}
}
}

View File

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

View File

@ -213,13 +213,14 @@ public class SampleActuatorApplicationTests {
@SuppressWarnings("unchecked")
public void testBeans() throws Exception {
@SuppressWarnings("rawtypes")
ResponseEntity<List> entity = this.restTemplate
ResponseEntity<Map> 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<String, Object> body = (Map<String, Object>) entity.getBody().get(0);
assertThat(((String) body.get("context"))).startsWith("application");
Map<String, Object> body = (Map<String, Object>) ((List<?>) entity.getBody()
.get("contexts")).get(0);
assertThat(((String) body.get("id"))).startsWith("application");
}
@Test