From 32818515b7b18840aad69985a0fe81d8cd649138 Mon Sep 17 00:00:00 2001 From: Christian Dupuis Date: Wed, 18 Dec 2013 19:02:49 +0100 Subject: [PATCH] Expose endpoints via JMX Actuator endpoints are now being exposed over JMX. --- .../EndpointMBeanExportAutoConfiguration.java | 36 +++++ .../actuate/endpoint/jmx/EndpointMBean.java | 70 +++++++++ .../endpoint/jmx/EndpointMBeanExporter.java | 111 ++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + ...ointMBeanExportAutoConfigurationTests.java | 69 +++++++++ .../jmx/EndpointMBeanExporterTests.java | 136 ++++++++++++++++++ 6 files changed, 423 insertions(+) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfigurationTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java new file mode 100644 index 0000000000..7feb5af07f --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013 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.autoconfigure; + +import org.springframework.boot.actuate.endpoint.jmx.EndpointMBeanExporter; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jmx.export.MBeanExporter; + +@Configuration +@ConditionalOnBean({ MBeanExporter.class }) +@AutoConfigureAfter({ EndpointAutoConfiguration.class }) +class EndpointMBeanExportAutoConfiguration { + + @Bean + public EndpointMBeanExporter endpointMBeanExporter() { + return new EndpointMBeanExporter(); + } + +} \ No newline at end of file diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java new file mode 100644 index 0000000000..47866be975 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013 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.jmx; + +import java.util.List; +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.util.ClassUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Simple wrapper around {@link Endpoint} implementations to enable JMX export. + * + * @author Christian Dupuis + */ +@ManagedResource +public class EndpointMBean { + + private Endpoint endpoint; + + private ObjectMapper mapper = new ObjectMapper(); + + public EndpointMBean(Endpoint endpoint) { + this.endpoint = endpoint; + } + + @ManagedAttribute(description = "Returns the class of the underlying endpoint") + public String getEndpointClass() { + return ClassUtils.getQualifiedName(this.endpoint.getClass()); + } + + @ManagedAttribute(description = "Indicates whether the underlying endpoint exposes sensitive information") + public boolean isSensitive() { + return this.endpoint.isSensitive(); + } + + @ManagedOperation(description = "Invoke the underlying endpoint") + public Object invoke() { + Object result = this.endpoint.invoke(); + if (result == null) { + return null; + } + else if (result instanceof String) { + return result; + } + else if (result.getClass().isArray() || result instanceof List) { + return this.mapper.convertValue(result, List.class); + } + return this.mapper.convertValue(result, Map.class); + } +} \ No newline at end of file diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java new file mode 100644 index 0000000000..7e6bdb8880 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013 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.jmx; + +import java.util.Map; + +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.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.jmx.export.MBeanExportException; +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link ApplicationListener} that registers all known {@link Endpoint}s with an + * {@link MBeanServer} using the {@link MBeanExporter} located from the application + * context. + * + * @author Christian Dupuis + */ +public class EndpointMBeanExporter implements ApplicationListener { + + private static final String DEFAULT_DOMAIN_NAME = ClassUtils + .getPackageName(Endpoint.class); + + private static Log logger = LogFactory.getLog(EndpointMBeanExporter.class); + + private String domainName = DEFAULT_DOMAIN_NAME; + + private String key = "bean"; + + public void setDomainName(String domainName) { + Assert.notNull(domainName, "DomainName should not be null"); + this.domainName = domainName; + } + + public void setKey(String key) { + Assert.notNull(key, "Key should not be null"); + this.key = key; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + ApplicationContext applicationContext = event.getApplicationContext(); + try { + MBeanExporter mbeanExporter = applicationContext.getBean(MBeanExporter.class); + locateAndRegisterEndpoints(applicationContext, mbeanExporter); + } + catch (NoSuchBeanDefinitionException nsbde) { + if (logger.isDebugEnabled()) { + logger.debug("Could not obtain MBeanExporter. No Endpoint JMX export will be attemted."); + } + } + } + + @SuppressWarnings({ "rawtypes" }) + protected void locateAndRegisterEndpoints(ApplicationContext applicationContext, + MBeanExporter mbeanExporter) { + Assert.notNull(applicationContext, "ApplicationContext should not be null"); + Map endpoints = applicationContext + .getBeansOfType(Endpoint.class); + for (Map.Entry endpointEntry : endpoints.entrySet()) { + registerEndpoint(endpointEntry.getKey(), endpointEntry.getValue(), + mbeanExporter); + } + } + + protected void registerEndpoint(String beanKey, Endpoint endpoint, + MBeanExporter mbeanExporter) { + try { + mbeanExporter.registerManagedResource(new EndpointMBean(endpoint), + getObjectName(beanKey, endpoint)); + } + catch (MBeanExportException e) { + logger.error("Could not register MBean for endpoint [" + beanKey + "]", e); + } + catch (MalformedObjectNameException e) { + logger.error("Could not register MBean for endpoint [" + beanKey + "]", e); + } + } + + protected ObjectName getObjectName(String beanKey, Endpoint endpoint) + throws MalformedObjectNameException { + return ObjectNameManager.getInstance(this.domainName, this.key, beanKey); + } + +} diff --git a/spring-boot-actuator/src/main/resources/META-INF/spring.factories b/spring-boot-actuator/src/main/resources/META-INF/spring.factories index bc44be858f..9993bce5f2 100644 --- a/spring-boot-actuator/src/main/resources/META-INF/spring.factories +++ b/spring-boot-actuator/src/main/resources/META-INF/spring.factories @@ -2,6 +2,7 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.CrshAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.EndpointMBeanExportAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.ErrorMvcAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.JolokiaAutoConfiguration,\ diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfigurationTests.java new file mode 100644 index 0000000000..7f83e19c10 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfigurationTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2013 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.autoconfigure; + +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.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableMBeanExport; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +/** + * Tests for {@link EndpointMBeanExportAutoConfiguration}. + * + */ +public class EndpointMBeanExportAutoConfigurationTests { + + private AnnotationConfigApplicationContext context; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void testEndpointMBeanExporterIsInstalled() { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(TestConfiguration.class, EndpointAutoConfiguration.class, + EndpointMBeanExportAutoConfiguration.class); + this.context.refresh(); + assertNotNull(this.context.getBean(EndpointMBeanExporter.class)); + } + + @Test(expected = NoSuchBeanDefinitionException.class) + public void testEndpointMBeanExporterIsNotInstalled() { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(EndpointAutoConfiguration.class, + EndpointMBeanExportAutoConfiguration.class); + this.context.refresh(); + this.context.getBean(EndpointMBeanExporter.class); + fail(); + } + + @Configuration + @EnableMBeanExport + public static class TestConfiguration { + + } +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java new file mode 100644 index 0000000000..f0314576b7 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2013 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.jmx; + +import java.util.HashMap; +import java.util.Map; + +import javax.management.MBeanInfo; + +import org.junit.After; +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.context.support.GenericApplicationContext; +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.jmx.support.ObjectNameManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Tests for {@link EndpointMBeanExporter} + * + * @author Christian Dupuis + */ +public class EndpointMBeanExporterTests { + + GenericApplicationContext context = null; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void testRegistrationOfOneEndpoint() throws Exception { + this.context = new GenericApplicationContext(); + this.context.registerBeanDefinition("endpointMbeanExporter", + new RootBeanDefinition(EndpointMBeanExporter.class)); + this.context.registerBeanDefinition("endpoint1", new RootBeanDefinition( + TestEndpoint.class)); + this.context.registerBeanDefinition("mbeanExporter", new RootBeanDefinition( + MBeanExporter.class)); + this.context.refresh(); + + MBeanExporter mbeanExporter = this.context.getBean(MBeanExporter.class); + + MBeanInfo mbeanInfo = mbeanExporter.getServer() + .getMBeanInfo( + ObjectNameManager.getInstance( + "org.springframework.boot.actuate.endpoint", "bean", + "endpoint1")); + assertNotNull(mbeanInfo); + assertEquals(3, mbeanInfo.getOperations().length); + assertEquals(2, mbeanInfo.getAttributes().length); + } + + @Test + public void testRegistrationTwoEndpoints() throws Exception { + this.context = new GenericApplicationContext(); + this.context.registerBeanDefinition("endpointMbeanExporter", + new RootBeanDefinition(EndpointMBeanExporter.class)); + this.context.registerBeanDefinition("endpoint1", new RootBeanDefinition( + TestEndpoint.class)); + this.context.registerBeanDefinition("endpoint2", new RootBeanDefinition( + TestEndpoint.class)); + this.context.registerBeanDefinition("mbeanExporter", new RootBeanDefinition( + MBeanExporter.class)); + this.context.refresh(); + + MBeanExporter mbeanExporter = this.context.getBean(MBeanExporter.class); + + assertNotNull(mbeanExporter.getServer() + .getMBeanInfo( + ObjectNameManager.getInstance( + "org.springframework.boot.actuate.endpoint", "bean", + "endpoint1"))); + assertNotNull(mbeanExporter.getServer() + .getMBeanInfo( + ObjectNameManager.getInstance( + "org.springframework.boot.actuate.endpoint", "bean", + "endpoint2"))); + } + + @Test + public void testRegistrationWithCustomDomainAndKey() throws Exception { + Map propertyValues = new HashMap(); + propertyValues.put("domainName", "test.domain"); + propertyValues.put("key", "key"); + + this.context = new GenericApplicationContext(); + this.context.registerBeanDefinition("endpointMbeanExporter", + new RootBeanDefinition(EndpointMBeanExporter.class, null, + new MutablePropertyValues(propertyValues))); + this.context.registerBeanDefinition("endpoint1", new RootBeanDefinition( + TestEndpoint.class)); + this.context.registerBeanDefinition("mbeanExporter", new RootBeanDefinition( + MBeanExporter.class)); + this.context.refresh(); + + MBeanExporter mbeanExporter = this.context.getBean(MBeanExporter.class); + + assertNotNull(mbeanExporter.getServer().getMBeanInfo( + ObjectNameManager.getInstance("test.domain", "key", "endpoint1"))); + } + + public static class TestEndpoint extends AbstractEndpoint { + + public TestEndpoint() { + super("/test"); + } + + @Override + protected String doInvoke() { + return "hello world"; + } + } + +}