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
This commit is contained in:
Andy Wilkinson 2017-01-16 11:44:20 +00:00
parent dbee44a6b5
commit a72365e1a2
2 changed files with 179 additions and 4 deletions

View File

@ -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<List<Object>>
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<List<Object>>
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<List<Object>>
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<ConfigurableApplicationContext> getContextHierarchy() {
Set<ConfigurableApplicationContext> contexts = new LinkedHashSet<ConfigurableApplicationContext>();
ApplicationContext context = this.leafContext;
while (context != null) {
contexts.add(asConfigurableContext(context));
context = context.getParent();
}
return contexts;
}
}
}

View File

@ -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<Object> 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<Object> 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<Object> {
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<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
static class EndpointConfiguration {
@Bean
public BeansEndpoint endpoint() {
return new BeansEndpoint();
}
}
@Configuration
static class BeanConfiguration {
@Bean
public String bean() {
return "bean";
}
}
}