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"; + } + + } + +}