diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/DefaultEndpointObjectNameFactory.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/DefaultEndpointObjectNameFactory.java index 6bd648e01ec..3cfc0835b8e 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/DefaultEndpointObjectNameFactory.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/DefaultEndpointObjectNameFactory.java @@ -16,12 +16,16 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.infrastructure; +import java.util.Map; + +import javax.management.MBeanServer; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import org.springframework.boot.endpoint.jmx.EndpointMBean; import org.springframework.boot.endpoint.jmx.EndpointObjectNameFactory; import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -32,15 +36,50 @@ import org.springframework.util.StringUtils; */ class DefaultEndpointObjectNameFactory implements EndpointObjectNameFactory { - private String domain = "org.springframework.boot"; + private final JmxEndpointExporterProperties properties; + + private final MBeanServer mBeanServer; + + private final String contextId; + + DefaultEndpointObjectNameFactory(JmxEndpointExporterProperties properties, + MBeanServer mBeanServer, String contextId) { + this.properties = properties; + this.mBeanServer = mBeanServer; + this.contextId = contextId; + } @Override public ObjectName generate(EndpointMBean mBean) throws MalformedObjectNameException { - StringBuilder builder = new StringBuilder(); - builder.append(this.domain); - builder.append(":type=Endpoint"); - builder.append(",name=" + StringUtils.capitalize(mBean.getEndpointId())); + String baseObjectName = this.properties.getDomain() + + ":type=Endpoint" + + ",name=" + StringUtils.capitalize(mBean.getEndpointId()); + StringBuilder builder = new StringBuilder(baseObjectName); + if (this.mBeanServer != null && hasMBean(baseObjectName)) { + builder.append(",context=").append(this.contextId); + } + if (this.properties.isUniqueNames()) { + builder.append(",identity=").append(ObjectUtils.getIdentityHexString(mBean)); + } + builder.append(getStaticNames()); return ObjectNameManager.getInstance(builder.toString()); } + private boolean hasMBean(String baseObjectName) throws MalformedObjectNameException { + ObjectName query = new ObjectName(baseObjectName + ",*"); + return this.mBeanServer.queryNames(query, null).size() > 0; + } + + private String getStaticNames() { + if (this.properties.getStaticNames().isEmpty()) { + return ""; + } + StringBuilder builder = new StringBuilder(); + + for (Map.Entry name : this.properties.getStaticNames().entrySet()) { + builder.append(",").append(name.getKey()).append("=").append(name.getValue()); + } + return builder.toString(); + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/EndpointInfrastructureAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/EndpointInfrastructureAutoConfiguration.java index 9bd3a32bf49..a228d133d3f 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/EndpointInfrastructureAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/EndpointInfrastructureAutoConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper; import org.springframework.boot.endpoint.EndpointType; import org.springframework.boot.endpoint.OperationParameterMapper; @@ -44,6 +45,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -56,6 +58,7 @@ import org.springframework.util.StringUtils; */ @Configuration @AutoConfigureAfter(JmxAutoConfiguration.class) +@EnableConfigurationProperties(JmxEndpointExporterProperties.class) public class EndpointInfrastructureAutoConfiguration { private final ApplicationContext applicationContext; @@ -94,18 +97,20 @@ public class EndpointInfrastructureAutoConfiguration { @ConditionalOnSingleCandidate(MBeanServer.class) @Bean - public JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, - JmxAnnotationEndpointDiscoverer endpointDiscoverer, + public JmxEndpointExporter jmxMBeanExporter(JmxEndpointExporterProperties properties, + MBeanServer mBeanServer, JmxAnnotationEndpointDiscoverer endpointDiscoverer, ObjectProvider objectMapper) { EndpointProvider endpointProvider = new EndpointProvider<>( this.applicationContext.getEnvironment(), endpointDiscoverer, EndpointType.JMX); EndpointMBeanRegistrar endpointMBeanRegistrar = new EndpointMBeanRegistrar( - mBeanServer, new DefaultEndpointObjectNameFactory()); + mBeanServer, new DefaultEndpointObjectNameFactory(properties, + mBeanServer, ObjectUtils.getIdentityHexString(this.applicationContext))); return new JmxEndpointExporter(endpointProvider, endpointMBeanRegistrar, objectMapper.getIfAvailable(ObjectMapper::new)); } + @Configuration @ConditionalOnWebApplication static class WebInfrastructureConfiguration { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/JmxEndpointExporterProperties.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/JmxEndpointExporterProperties.java new file mode 100644 index 00000000000..c079a7e7dfe --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/JmxEndpointExporterProperties.java @@ -0,0 +1,77 @@ +/* + * 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.autoconfigure.endpoint.infrastructure; + +import java.util.Properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +/** + * Configuration properties for JMX export of endpoints. + * + * @author Stephane Nicoll + * @since 2.0.0 + */ +@ConfigurationProperties("management.jmx") +public class JmxEndpointExporterProperties { + + /** + * Endpoints JMX domain name. Fallback to 'spring.jmx.default-domain' if set. + */ + private String domain = "org.springframework.boot"; + + /** + * Ensure that ObjectNames are modified in case of conflict. + */ + private boolean uniqueNames = false; + + /** + * Additional static properties to append to all ObjectNames of MBeans representing + * Endpoints. + */ + private final Properties staticNames = new Properties(); + + public JmxEndpointExporterProperties(Environment environment) { + String defaultDomain = environment.getProperty("spring.jmx.default-domain"); + if (StringUtils.hasText(defaultDomain)) { + this.domain = defaultDomain; + } + } + + public String getDomain() { + return this.domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public boolean isUniqueNames() { + return this.uniqueNames; + } + + public void setUniqueNames(boolean uniqueNames) { + this.uniqueNames = uniqueNames; + } + + public Properties getStaticNames() { + return this.staticNames; + } + +} diff --git a/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 752a0e348e1..96d1fd3770b 100644 --- a/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -52,12 +52,6 @@ "type": "java.lang.String", "description": "Endpoint URL path." }, - { - "name": "endpoints.jmx.enabled", - "type": "java.lang.Boolean", - "description": "Enable JMX export of all endpoints.", - "defaultValue": true - }, { "name": "endpoints.mappings.path", "type": "java.lang.String", diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/DefaultEndpointObjectNameFactoryTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/DefaultEndpointObjectNameFactoryTests.java new file mode 100644 index 00000000000..728c824408c --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/DefaultEndpointObjectNameFactoryTests.java @@ -0,0 +1,122 @@ +/* + * 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.autoconfigure.endpoint.infrastructure; + +import java.util.Collections; + +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.junit.Test; + +import org.springframework.boot.endpoint.jmx.EndpointMBean; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DefaultEndpointObjectNameFactory}. + * + * @author Stephane Nicoll + */ +public class DefaultEndpointObjectNameFactoryTests { + + private final MockEnvironment environment = new MockEnvironment(); + + private final JmxEndpointExporterProperties properties = new JmxEndpointExporterProperties(this.environment); + + private final MBeanServer mBeanServer = mock(MBeanServer.class); + + private String contextId; + + @Test + public void generateObjectName() { + ObjectName objectName = generateObjectName(endpoint("Test")); + assertThat(objectName.toString()).isEqualTo( + "org.springframework.boot:type=Endpoint,name=Test"); + } + + @Test + public void generateObjectNameWithCapitalizedId() { + ObjectName objectName = generateObjectName(endpoint("test")); + assertThat(objectName.toString()).isEqualTo( + "org.springframework.boot:type=Endpoint,name=Test"); + } + + @Test + public void generateObjectNameWithCustomDomain() { + this.properties.setDomain("com.example.acme"); + ObjectName objectName = generateObjectName(endpoint("test")); + assertThat(objectName.toString()).isEqualTo( + "com.example.acme:type=Endpoint,name=Test"); + } + + @Test + public void generateObjectNameWithUniqueNames() { + this.properties.setUniqueNames(true); + EndpointMBean endpoint = endpoint("test"); + String id = ObjectUtils.getIdentityHexString(endpoint); + ObjectName objectName = generateObjectName(endpoint); + assertThat(objectName.toString()).isEqualTo( + "org.springframework.boot:type=Endpoint,name=Test,identity=" + id); + } + + @Test + public void generateObjectNameWithStaticNames() { + this.properties.getStaticNames().setProperty("counter", "42"); + this.properties.getStaticNames().setProperty("foo", "bar"); + ObjectName objectName = generateObjectName(endpoint("test")); + assertThat(objectName.getKeyProperty("counter")).isEqualTo("42"); + assertThat(objectName.getKeyProperty("foo")).isEqualTo("bar"); + assertThat(objectName.toString()).startsWith( + "org.springframework.boot:type=Endpoint,name=Test,"); + } + + @Test + public void generateObjectNameWithDuplicate() throws MalformedObjectNameException { + this.contextId = "testContext"; + given(this.mBeanServer.queryNames(new ObjectName( + "org.springframework.boot:type=Endpoint,name=Test,*"), null)) + .willReturn(Collections.singleton( + new ObjectName("org.springframework.boot:type=Endpoint,name=Test"))); + ObjectName objectName = generateObjectName(endpoint("test")); + assertThat(objectName.toString()).isEqualTo( + "org.springframework.boot:type=Endpoint,name=Test,context=testContext"); + + } + + private ObjectName generateObjectName(EndpointMBean endpointMBean) { + try { + return new DefaultEndpointObjectNameFactory(this.properties, this.mBeanServer, + this.contextId).generate(endpointMBean); + } + catch (MalformedObjectNameException ex) { + throw new AssertionError("Invalid object name", ex); + } + } + + private EndpointMBean endpoint(String id) { + EndpointMBean endpointMBean = mock(EndpointMBean.class); + given(endpointMBean.getEndpointId()).willReturn(id); + return endpointMBean; + } + +} diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 563c71fe9c8..0a6d0dd07e4 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -1119,17 +1119,6 @@ content into your application; rather pick only the properties that you need. endpoints.cors.exposed-headers= # Comma-separated list of headers to include in a response. endpoints.cors.max-age=1800 # How long, in seconds, the response from a pre-flight request can be cached by clients. - # JMX ENDPOINT ({sc-spring-boot-actuator}/autoconfigure/EndpointMBeanExportProperties.{sc-ext}[EndpointMBeanExportProperties]) - endpoints.jmx.domain= # JMX domain name. Initialized with the value of 'spring.jmx.default-domain' if set. - endpoints.jmx.enabled=true # Enable JMX export of all endpoints. - endpoints.jmx.static-names= # Additional static properties to append to all ObjectNames of MBeans representing Endpoints. - endpoints.jmx.unique-names=false # Ensure that ObjectNames are modified in case of conflict. - - # JOLOKIA ({sc-spring-boot-actuator}/autoconfigure/jolokia/JolokiaProperties.{sc-ext}[JolokiaProperties]) - management.jolokia.config.*= # Jolokia settings. See the Jolokia manual for details. - management.jolokia.enabled=true # Enable Jolokia. - management.jolokia.path=/jolokia # Path at which Jolokia will be available. - # MANAGEMENT HTTP SERVER ({sc-spring-boot-actuator}/autoconfigure/ManagementServerProperties.{sc-ext}[ManagementServerProperties]) management.add-application-context-header=false # Add the "X-Application-Context" HTTP header in each response. management.address= # Network address that the management endpoints should bind to. @@ -1184,6 +1173,16 @@ content into your application; rather pick only the properties that you need. management.info.git.enabled=true # Enable git info. management.info.git.mode=simple # Mode to use to expose git information. + # JMX ENDPOINT ({sc-spring-boot-actuator}/autoconfigure/endpoint.infrastructure/JmxEndpointExporterProperties.{sc-ext}[JmxEndpointExporterProperties]) + management.jmx.domain=org.springframework.boot # Endpoints JMX domain name. Fallback to 'spring.jmx.default-domain' if set. + management.jmx.static-names=false # Additional static properties to append to all ObjectNames of MBeans representing Endpoints. + management.jmx.unique-names=false # Ensure that ObjectNames are modified in case of conflict. + + # JOLOKIA ({sc-spring-boot-actuator}/autoconfigure/jolokia/JolokiaProperties.{sc-ext}[JolokiaProperties]) + management.jolokia.config.*= # Jolokia settings. See the Jolokia manual for details. + management.jolokia.enabled=true # Enable Jolokia. + management.jolokia.path=/jolokia # Path at which Jolokia will be available. + # TRACING ({sc-spring-boot-actuator}/trace/TraceProperties.{sc-ext}[TraceProperties]) management.trace.include=request-headers,response-headers,cookies,errors # Items to be included in the trace. diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index a459d12eb93..c12b1d6c915 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -734,10 +734,10 @@ under the `org.springframework.boot` domain. [[production-ready-custom-mbean-names]] === Customizing MBean names The name of the MBean is usually generated from the `id` of the endpoint. For example -the `health` endpoint is exposed as `org.springframework.boot/Endpoint/healthEndpoint`. +the `health` endpoint is exposed as `org.springframework.boot:type=Endpoint,name=Health`. If your application contains more than one Spring `ApplicationContext` you may find that -names clash. To solve this problem you can set the `endpoints.jmx.unique-names` property +names clash. To solve this problem you can set the `management.jmx.unique-names` property to `true` so that MBean names are always unique. You can also customize the JMX domain under which endpoints are exposed. Here is an @@ -745,20 +745,20 @@ example `application.properties`: [source,properties,indent=0] ---- - endpoints.jmx.domain=myapp - endpoints.jmx.unique-names=true + management.jmx.domain=com.example.myapp + management.jmx.unique-names=true ---- [[production-ready-disable-jmx-endpoints]] === Disabling JMX endpoints -If you don't want to expose endpoints over JMX you can set the `endpoints.jmx.enabled` +If you don't want to expose endpoints over JMX you can set the `endpoints.all.jmx.enabled` property to `false`: [source,properties,indent=0] ---- - endpoints.jmx.enabled=false + endpoints.all.jmx.enabled=false ----