Improve structure and JSON serialization of beans endpoint's response
Closes gh-10156
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user