From a72365e1a29ca2e6f115c55b49919cb013e46a9c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Jan 2017 11:44:20 +0000 Subject: [PATCH] Expose context hierarchy in beans endpoint Previously, the beans endpoint would only expose the context that contained the endpoint. This commit updates the endpoint so that the context that contains the endpoint and all of its ancestors are exposed. In a context hierarhcy, the relation ship is child -> parent and there is no way to navigate from a parent to a child. As a result, any contexts that are descendants of the context containing the endpoint are not exposed. Closes gh-5188 --- .../boot/actuate/endpoint/BeansEndpoint.java | 46 +++++- .../BeansEndpointContextHierarchyTests.java | 137 ++++++++++++++++++ 2 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointContextHierarchyTests.java 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 68aa2367f2..737e1b62a5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package org.springframework.boot.actuate.endpoint; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.springframework.beans.BeansException; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -24,22 +26,25 @@ 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; /** * 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. + * behavior of LiveBeansView). Otherwise only the current application context hierarchy. * * @author Dave Syer + * @author Andy Wilkinson */ @ConfigurationProperties(prefix = "endpoints.beans") public class BeansEndpoint extends AbstractEndpoint> implements ApplicationContextAware { - private final LiveBeansView liveBeansView = new LiveBeansView(); + private final HierarchyAwareLiveBeansView liveBeansView = new HierarchyAwareLiveBeansView(); private final JsonParser parser = JsonParserFactory.getJsonParser(); @@ -51,7 +56,7 @@ public class BeansEndpoint extends AbstractEndpoint> public void setApplicationContext(ApplicationContext context) throws BeansException { if (context.getEnvironment() .getProperty(LiveBeansView.MBEAN_DOMAIN_PROPERTY_NAME) == null) { - this.liveBeansView.setApplicationContext(context); + this.liveBeansView.setLeafContext(context); } } @@ -60,4 +65,37 @@ public class BeansEndpoint extends AbstractEndpoint> return this.parser.parseList(this.liveBeansView.getSnapshotAsJson()); } + private static class HierarchyAwareLiveBeansView extends LiveBeansView { + + private ConfigurableApplicationContext leafContext; + + private void setLeafContext(ApplicationContext leafContext) { + this.leafContext = asConfigurableContext(leafContext); + } + + @Override + public String getSnapshotAsJson() { + return generateJson(getContextHierarchy()); + } + + private ConfigurableApplicationContext asConfigurableContext( + ApplicationContext applicationContext) { + Assert.isTrue(applicationContext instanceof ConfigurableApplicationContext, + "'" + applicationContext + + "' does not implement ConfigurableApplicationContext"); + return (ConfigurableApplicationContext) applicationContext; + } + + private Set getContextHierarchy() { + Set contexts = new LinkedHashSet(); + ApplicationContext context = this.leafContext; + while (context != null) { + contexts.add(asConfigurableContext(context)); + context = context.getParent(); + } + return contexts; + } + + } + } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointContextHierarchyTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointContextHierarchyTests.java new file mode 100644 index 0000000000..dea3ce94b6 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointContextHierarchyTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint; + +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.Condition; +import org.junit.After; +import org.junit.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BeansEndpoint} with an application context hierarchy. + * + * @author Andy Wilkinson + */ +public class BeansEndpointContextHierarchyTests { + + private AnnotationConfigApplicationContext parent; + + private AnnotationConfigApplicationContext child; + + @After + public void closeContexts() { + if (this.child != null) { + this.child.close(); + } + if (this.parent != null) { + this.parent.close(); + } + } + + @Test + public void beansInParentContextAreFound() { + load(BeanConfiguration.class, EndpointConfiguration.class); + BeansEndpoint endpoint = this.child.getBean(BeansEndpoint.class); + List contexts = endpoint.invoke(); + assertThat(contexts).hasSize(2); + assertThat(contexts.get(1)).has(beanNamed("bean")); + assertThat(contexts.get(0)).has(beanNamed("endpoint")); + } + + @Test + public void beansInChildContextAreNotFound() { + load(EndpointConfiguration.class, BeanConfiguration.class); + BeansEndpoint endpoint = this.child.getBean(BeansEndpoint.class); + List contexts = endpoint.invoke(); + assertThat(contexts).hasSize(1); + assertThat(contexts.get(0)).has(beanNamed("endpoint")); + assertThat(contexts.get(0)).doesNotHave(beanNamed("bean")); + } + + private void load(Class parent, Class child) { + this.parent = new AnnotationConfigApplicationContext(parent); + this.child = new AnnotationConfigApplicationContext(); + this.child.setParent(this.parent); + this.child.register(child); + this.child.refresh(); + } + + private ContextHasBeanCondition beanNamed(String beanName) { + return new ContextHasBeanCondition(beanName); + } + + private static final class ContextHasBeanCondition extends Condition { + + private final String beanName; + + private ContextHasBeanCondition(String 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 + static class EndpointConfiguration { + + @Bean + public BeansEndpoint endpoint() { + return new BeansEndpoint(); + } + + } + + @Configuration + static class BeanConfiguration { + + @Bean + public String bean() { + return "bean"; + } + + } + +}