Commit 65d6757a authored by Christian Dupuis's avatar Christian Dupuis

Rework EndpointMBeanExporter to prevent name clashes and to provide more...

Rework EndpointMBeanExporter to prevent name clashes and to provide more flexibility in naming of endpoint MBeans
parent b81930fc
......@@ -16,15 +16,19 @@
package org.springframework.boot.actuate.autoconfigure;
import java.util.Properties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.jmx.EndpointMBeanExporter;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} to enable JMX export for
......@@ -33,13 +37,41 @@ import org.springframework.jmx.export.MBeanExporter;
* @author Christian Dupuis
*/
@Configuration
@ConditionalOnBean({ MBeanExporter.class })
@AutoConfigureAfter({ EndpointAutoConfiguration.class })
@ConditionalOnExpression("${endpoints.jmx.enabled:true}")
class EndpointMBeanExportAutoConfiguration {
private RelaxedPropertyResolver environment;
@Autowired
public void setEnvironment(Environment environment) {
this.environment = new RelaxedPropertyResolver(environment);
}
@Bean
public EndpointMBeanExporter endpointMBeanExporter() {
return new EndpointMBeanExporter();
EndpointMBeanExporter mbeanExporter = new EndpointMBeanExporter();
String domain = this.environment.getProperty("endpoints.jmx.domain");
if (StringUtils.hasText(domain)) {
mbeanExporter.setDomain(domain);
}
Boolean ensureUnique = this.environment.getProperty("endpoints.jmx.unique_names",
Boolean.class, Boolean.FALSE);
mbeanExporter.setEnsureUniqueRuntimeObjectNames(ensureUnique);
mbeanExporter.setObjectNameStaticProperties(getObjectNameStaticProperties());
return mbeanExporter;
}
private Properties getObjectNameStaticProperties() {
String staticNames = this.environment.getProperty("endpoints.jmx.static_names");
if (StringUtils.hasText(staticNames)) {
return StringUtils.splitArrayElementsIntoProperties(
StringUtils.commaDelimitedListToStringArray(staticNames), "=");
}
return new Properties();
}
}
\ No newline at end of file
......@@ -19,15 +19,9 @@ package org.springframework.boot.actuate.endpoint.jmx;
import java.util.List;
import java.util.Map;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.jmx.export.naming.MetadataNamingStrategy;
import org.springframework.jmx.export.naming.SelfNaming;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
......@@ -39,24 +33,16 @@ import com.fasterxml.jackson.databind.ObjectMapper;
* @author Christian Dupuis
*/
@ManagedResource
public class EndpointMBean implements SelfNaming {
private AnnotationJmxAttributeSource annotationSource = new AnnotationJmxAttributeSource();
private MetadataNamingStrategy metadataNamingStrategy = new MetadataNamingStrategy(
this.annotationSource);
public class EndpointMBean {
private Endpoint<?> endpoint;
private String beanName;
private ObjectMapper mapper = new ObjectMapper();
public EndpointMBean(String beanName, Endpoint<?> endpoint) {
Assert.notNull(beanName, "BeanName must not be null");
Assert.notNull(endpoint, "Endpoint must not be null");
this.endpoint = endpoint;
this.beanName = beanName;
}
@ManagedAttribute(description = "Returns the class of the underlying endpoint")
......@@ -69,11 +55,6 @@ public class EndpointMBean implements SelfNaming {
return this.endpoint.isSensitive();
}
@Override
public ObjectName getObjectName() throws MalformedObjectNameException {
return this.metadataNamingStrategy.getObjectName(this, this.beanName);
}
public Endpoint<?> getEndpoint() {
return this.endpoint;
}
......
......@@ -18,24 +18,31 @@ package org.springframework.boot.actuate.endpoint.jmx;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.ShutdownEndpoint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.SmartLifecycle;
import org.springframework.jmx.export.MBeanExportException;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.util.Assert;
import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
import org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler;
import org.springframework.jmx.export.naming.MetadataNamingStrategy;
import org.springframework.jmx.export.naming.SelfNaming;
import org.springframework.jmx.support.ObjectNameManager;
import org.springframework.util.ObjectUtils;
/**
* {@link ApplicationListener} that registers all known {@link Endpoint}s with an
......@@ -44,10 +51,21 @@ import org.springframework.util.Assert;
*
* @author Christian Dupuis
*/
public class EndpointMBeanExporter implements SmartLifecycle, ApplicationContextAware {
public class EndpointMBeanExporter extends MBeanExporter implements SmartLifecycle,
BeanFactoryAware {
public static final String DEFAULT_DOMAIN = "org.springframework.boot";
private static Log logger = LogFactory.getLog(EndpointMBeanExporter.class);
private final AnnotationJmxAttributeSource attributeSource = new AnnotationJmxAttributeSource();
private final MetadataMBeanInfoAssembler assembler = new MetadataMBeanInfoAssembler(
this.attributeSource);
private final MetadataNamingStrategy defaultNamingStrategy = new MetadataNamingStrategy(
this.attributeSource);
private Set<Endpoint<?>> registeredEndpoints = new HashSet<Endpoint<?>>();
private volatile boolean autoStartup = true;
......@@ -58,45 +76,65 @@ public class EndpointMBeanExporter implements SmartLifecycle, ApplicationContext
private final ReentrantLock lifecycleLock = new ReentrantLock();
private ApplicationContext applicationContext;
private ListableBeanFactory beanFactory;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
private String domain = DEFAULT_DOMAIN;
private boolean ensureUniqueRuntimeObjectNames = false;
private Properties objectNameStaticProperties = new Properties();
public EndpointMBeanExporter() {
super();
setAutodetect(false);
setNamingStrategy(this.defaultNamingStrategy);
setAssembler(this.assembler);
}
protected void doStart() {
try {
MBeanExporter mbeanExporter = this.applicationContext
.getBean(MBeanExporter.class);
locateAndRegisterEndpoints(mbeanExporter);
@Override
public void setBeanFactory(BeanFactory beanFactory) {
super.setBeanFactory(beanFactory);
if (beanFactory instanceof ListableBeanFactory) {
this.beanFactory = (ListableBeanFactory) beanFactory;
}
catch (NoSuchBeanDefinitionException nsbde) {
if (logger.isDebugEnabled()) {
logger.debug("Could not obtain MBeanExporter. No Endpoint JMX export will be attemted.");
}
else {
logger.info("EndpointMBeanExporter not running in a ListableBeanFactory: "
+ "autodetection of Endpoints not available.");
}
}
public void setDomain(String domain) {
this.domain = domain;
}
@Override
public void setEnsureUniqueRuntimeObjectNames(boolean ensureUniqueRuntimeObjectNames) {
super.setEnsureUniqueRuntimeObjectNames(ensureUniqueRuntimeObjectNames);
this.ensureUniqueRuntimeObjectNames = ensureUniqueRuntimeObjectNames;
}
public void setObjectNameStaticProperties(Properties objectNameStaticProperties) {
this.objectNameStaticProperties = objectNameStaticProperties;
}
protected void doStart() {
locateAndRegisterEndpoints();
}
@SuppressWarnings({ "rawtypes" })
protected void locateAndRegisterEndpoints(MBeanExporter mbeanExporter) {
Assert.notNull(mbeanExporter, "MBeanExporter must not be null");
Map<String, Endpoint> endpoints = this.applicationContext
.getBeansOfType(Endpoint.class);
protected void locateAndRegisterEndpoints() {
Map<String, Endpoint> endpoints = this.beanFactory.getBeansOfType(Endpoint.class);
for (Map.Entry<String, Endpoint> endpointEntry : endpoints.entrySet()) {
if (!this.registeredEndpoints.contains(endpointEntry.getValue())) {
registerEndpoint(endpointEntry.getKey(), endpointEntry.getValue(),
mbeanExporter);
registerEndpoint(endpointEntry.getKey(), endpointEntry.getValue());
this.registeredEndpoints.add(endpointEntry.getValue());
}
}
}
protected void registerEndpoint(String beanName, Endpoint<?> endpoint,
MBeanExporter mbeanExporter) {
protected void registerEndpoint(String beanName, Endpoint<?> endpoint) {
try {
mbeanExporter.registerManagedResource(getEndpointMBean(beanName, endpoint));
registerBeanNameOrInstance(getEndpointMBean(beanName, endpoint), beanName);
}
catch (MBeanExportException ex) {
logger.error("Could not register MBean for endpoint [" + beanName + "]", ex);
......@@ -110,6 +148,42 @@ public class EndpointMBeanExporter implements SmartLifecycle, ApplicationContext
return new DataEndpointMBean(beanName, endpoint);
}
@Override
protected ObjectName getObjectName(Object bean, String beanKey)
throws MalformedObjectNameException {
if (bean instanceof SelfNaming) {
return ((SelfNaming) bean).getObjectName();
}
if (bean instanceof EndpointMBean) {
StringBuilder builder = new StringBuilder();
builder.append(this.domain);
builder.append(":type=Endpoint");
builder.append(",name=" + beanKey);
if (this.ensureUniqueRuntimeObjectNames) {
builder.append(",identity="
+ ObjectUtils.getIdentityHexString(((EndpointMBean) bean)
.getEndpoint()));
}
builder.append(getStaticNames());
return ObjectNameManager.getInstance(builder.toString());
}
return this.defaultNamingStrategy.getObjectName(bean, beanKey);
}
private String getStaticNames() {
if (this.objectNameStaticProperties.isEmpty()) {
return "";
}
StringBuilder builder = new StringBuilder();
for (Object key : this.objectNameStaticProperties.keySet()) {
builder.append("," + key + "=" + this.objectNameStaticProperties.get(key));
}
return builder.toString();
}
// SmartLifeCycle implementation
public final int getPhase() {
......
......@@ -16,13 +16,24 @@
package org.springframework.boot.actuate.autoconfigure;
import javax.management.InstanceNotFoundException;
import javax.management.IntrospectionException;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import org.junit.After;
import org.junit.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.actuate.endpoint.jmx.EndpointMBeanExporter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.jmx.support.ObjectNameManager;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.util.ObjectUtils;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
......@@ -53,7 +64,10 @@ public class EndpointMBeanExportAutoConfigurationTests {
@Test(expected = NoSuchBeanDefinitionException.class)
public void testEndpointMBeanExporterIsNotInstalled() {
MockEnvironment environment = new MockEnvironment();
environment.setProperty("endpoints.jmx.enabled", "false");
this.context = new AnnotationConfigApplicationContext();
this.context.setEnvironment(environment);
this.context.register(EndpointAutoConfiguration.class,
EndpointMBeanExportAutoConfiguration.class);
this.context.refresh();
......@@ -61,6 +75,35 @@ public class EndpointMBeanExportAutoConfigurationTests {
fail();
}
@Test
public void testEndpointMBeanExporterWithProperties() throws IntrospectionException,
InstanceNotFoundException, MalformedObjectNameException, ReflectionException {
MockEnvironment environment = new MockEnvironment();
environment.setProperty("endpoints.jmx.domain", "test-domain");
environment.setProperty("endpoints.jmx.unique_names", "true");
environment.setProperty("endpoints.jmx.static_names", "key1=value1, key2=value2");
this.context = new AnnotationConfigApplicationContext();
this.context.setEnvironment(environment);
this.context.register(EndpointAutoConfiguration.class,
EndpointMBeanExportAutoConfiguration.class);
this.context.refresh();
this.context.getBean(EndpointMBeanExporter.class);
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertNotNull(mbeanExporter.getServer().getMBeanInfo(
ObjectNameManager.getInstance(getObjectName("test-domain",
"healthEndpoint", this.context).toString()
+ ",key1=value1,key2=value2")));
}
private ObjectName getObjectName(String domain, String beanKey,
ApplicationContext applicationContext) throws MalformedObjectNameException {
return ObjectNameManager.getInstance(String.format(
"%s:type=Endpoint,name=%s,identity=%s", domain, beanKey,
ObjectUtils.getIdentityHexString(applicationContext.getBean(beanKey))));
}
@Configuration
@EnableMBeanExport
public static class TestConfiguration {
......
......@@ -17,6 +17,9 @@
package org.springframework.boot.actuate.endpoint.jmx;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.management.MBeanInfo;
import javax.management.MalformedObjectNameException;
......@@ -27,10 +30,11 @@ import org.junit.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.jmx.support.ObjectNameManager;
import org.springframework.util.ObjectUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
......@@ -58,20 +62,15 @@ public class EndpointMBeanExporterTests {
new RootBeanDefinition(EndpointMBeanExporter.class));
this.context.registerBeanDefinition("endpoint1", new RootBeanDefinition(
TestEndpoint.class));
this.context.registerBeanDefinition(
"mbeanExporter",
new RootBeanDefinition(MBeanExporter.class, null,
new MutablePropertyValues(Collections.singletonMap(
"ensureUniqueRuntimeObjectNames", "false"))));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(MBeanExporter.class);
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
MBeanInfo mbeanInfo = mbeanExporter.getServer().getMBeanInfo(
getObjectName("endpoint1", this.context));
assertNotNull(mbeanInfo);
assertEquals(5, mbeanInfo.getOperations().length);
assertEquals(5, mbeanInfo.getAttributes().length);
assertEquals(3, mbeanInfo.getOperations().length);
assertEquals(3, mbeanInfo.getAttributes().length);
}
@Test
......@@ -83,14 +82,9 @@ public class EndpointMBeanExporterTests {
TestEndpoint.class));
this.context.registerBeanDefinition("endpoint2", new RootBeanDefinition(
TestEndpoint.class));
this.context.registerBeanDefinition(
"mbeanExporter",
new RootBeanDefinition(MBeanExporter.class, null,
new MutablePropertyValues(Collections.singletonMap(
"ensureUniqueRuntimeObjectNames", "false"))));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(MBeanExporter.class);
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertNotNull(mbeanExporter.getServer().getMBeanInfo(
getObjectName("endpoint1", this.context)));
......@@ -98,6 +92,69 @@ public class EndpointMBeanExporterTests {
getObjectName("endpoint2", this.context)));
}
@Test
public void testRegistrationWithDifferentDomain() throws Exception {
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition(
"endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class, null,
new MutablePropertyValues(Collections.singletonMap("domain",
"test-domain"))));
this.context.registerBeanDefinition("endpoint1", new RootBeanDefinition(
TestEndpoint.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertNotNull(mbeanExporter.getServer().getMBeanInfo(
getObjectName("test-domain", "endpoint1", false, this.context)));
}
@Test
public void testRegistrationWithDifferentDomainAndIdentity() throws Exception {
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("domain", "test-domain");
properties.put("ensureUniqueRuntimeObjectNames", true);
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class, null,
new MutablePropertyValues(properties)));
this.context.registerBeanDefinition("endpoint1", new RootBeanDefinition(
TestEndpoint.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertNotNull(mbeanExporter.getServer().getMBeanInfo(
getObjectName("test-domain", "endpoint1", true, this.context)));
}
@Test
public void testRegistrationWithDifferentDomainAndIdentityAndStaticNames()
throws Exception {
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("domain", "test-domain");
properties.put("ensureUniqueRuntimeObjectNames", true);
Properties staticNames = new Properties();
staticNames.put("key1", "value1");
staticNames.put("key2", "value2");
properties.put("objectNameStaticProperties", staticNames);
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class, null,
new MutablePropertyValues(properties)));
this.context.registerBeanDefinition("endpoint1", new RootBeanDefinition(
TestEndpoint.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertNotNull(mbeanExporter.getServer().getMBeanInfo(
ObjectNameManager.getInstance(getObjectName("test-domain", "endpoint1",
true, this.context).toString()
+ ",key1=value1,key2=value2")));
}
@Test
public void testRegistrationWithParentContext() throws Exception {
this.context = new GenericApplicationContext();
......@@ -105,19 +162,13 @@ public class EndpointMBeanExporterTests {
new RootBeanDefinition(EndpointMBeanExporter.class));
this.context.registerBeanDefinition("endpoint1", new RootBeanDefinition(
TestEndpoint.class));
this.context.registerBeanDefinition(
"mbeanExporter",
new RootBeanDefinition(MBeanExporter.class, null,
new MutablePropertyValues(Collections.singletonMap(
"ensureUniqueRuntimeObjectNames", "false"))));
GenericApplicationContext parent = new GenericApplicationContext();
this.context.setParent(parent);
parent.refresh();
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(MBeanExporter.class);
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertNotNull(mbeanExporter.getServer().getMBeanInfo(
getObjectName("endpoint1", this.context)));
......@@ -125,10 +176,25 @@ public class EndpointMBeanExporterTests {
parent.close();
}
private ObjectName getObjectName(String beanKey, ApplicationContext applicationContext)
private ObjectName getObjectName(String beanKey, GenericApplicationContext context)
throws MalformedObjectNameException {
return new DataEndpointMBean(beanKey,
(Endpoint<?>) applicationContext.getBean(beanKey)).getObjectName();
return getObjectName("org.springframework.boot", beanKey, false, context);
}
private ObjectName getObjectName(String domain, String beanKey,
boolean includeIdentity, ApplicationContext applicationContext)
throws MalformedObjectNameException {
if (includeIdentity) {
return ObjectNameManager
.getInstance(String.format("%s:type=Endpoint,name=%s,identity=%s",
domain, beanKey, ObjectUtils
.getIdentityHexString(applicationContext
.getBean(beanKey))));
}
else {
return ObjectNameManager.getInstance(String.format(
"%s:type=Endpoint,name=%s", domain, beanKey));
}
}
public static class TestEndpoint extends AbstractEndpoint<String> {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment