diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java index e221b4465b..c611921d6f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java @@ -23,6 +23,7 @@ import javax.management.MBeanServer; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; import org.springframework.boot.actuate.endpoint.EndpointFilter; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -58,7 +59,7 @@ import org.springframework.util.ObjectUtils; * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@AutoConfigureAfter(JmxAutoConfiguration.class) +@AutoConfigureAfter({ JmxAutoConfiguration.class, EndpointAutoConfiguration.class }) @EnableConfigurationProperties(JmxEndpointProperties.class) @ConditionalOnProperty(prefix = "spring.jmx", name = "enabled", havingValue = "true") public class JmxEndpointAutoConfiguration { @@ -83,15 +84,21 @@ public class JmxEndpointAutoConfiguration { } @Bean - @ConditionalOnSingleCandidate(MBeanServer.class) - public JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, Environment environment, - ObjectProvider objectMapper, JmxEndpointsSupplier jmxEndpointsSupplier) { + @ConditionalOnMissingBean(EndpointObjectNameFactory.class) + public DefaultEndpointObjectNameFactory endpointObjectNameFactory(MBeanServer mBeanServer, + Environment environment) { String contextId = ObjectUtils.getIdentityHexString(this.applicationContext); - EndpointObjectNameFactory objectNameFactory = new DefaultEndpointObjectNameFactory(this.properties, environment, - mBeanServer, contextId); + return new DefaultEndpointObjectNameFactory(this.properties, environment, mBeanServer, contextId); + } + + @Bean + @ConditionalOnSingleCandidate(MBeanServer.class) + public JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, + EndpointObjectNameFactory endpointObjectNameFactory, ObjectProvider objectMapper, + JmxEndpointsSupplier jmxEndpointsSupplier) { JmxOperationResponseMapper responseMapper = new JacksonJmxOperationResponseMapper( objectMapper.getIfAvailable()); - return new JmxEndpointExporter(mBeanServer, objectNameFactory, responseMapper, + return new JmxEndpointExporter(mBeanServer, endpointObjectNameFactory, responseMapper, jmxEndpointsSupplier.getEndpoints()); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfigurationTests.java new file mode 100644 index 0000000000..7bb06ee23a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfigurationTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2021 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 + * + * https://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.jmx; + +import java.util.function.Function; + +import javax.management.MBeanServer; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.jmx.EndpointObjectNameFactory; +import org.springframework.boot.actuate.endpoint.jmx.ExposableJmxEndpoint; +import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointExporter; +import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpointDiscoverer; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link JmxEndpointAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class JmxEndpointAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class, JmxAutoConfiguration.class, + JmxEndpointAutoConfiguration.class)) + .withUserConfiguration(TestEndpoint.class); + + private final MBeanServer mBeanServer = mock(MBeanServer.class); + + @Test + void jmxEndpointWithoutJmxSupportNotAutoConfigured() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(MBeanServer.class) + .doesNotHaveBean(JmxEndpointDiscoverer.class).doesNotHaveBean(JmxEndpointExporter.class)); + } + + @Test + void jmxEndpointWithJmxSupportAutoConfigured() { + this.contextRunner.withPropertyValues("spring.jmx.enabled=true").with(mockMBeanServer()) + .run((context) -> assertThat(context).hasSingleBean(JmxEndpointDiscoverer.class) + .hasSingleBean(JmxEndpointExporter.class)); + } + + @Test + void jmxEndpointWithCustomEndpointObjectNameFactory() { + EndpointObjectNameFactory factory = mock(EndpointObjectNameFactory.class); + this.contextRunner.withPropertyValues("spring.jmx.enabled=true").with(mockMBeanServer()) + .withBean(EndpointObjectNameFactory.class, () -> factory).run((context) -> { + ArgumentCaptor argumentCaptor = ArgumentCaptor + .forClass(ExposableJmxEndpoint.class); + verify(factory).getObjectName(argumentCaptor.capture()); + ExposableJmxEndpoint jmxEndpoint = argumentCaptor.getValue(); + assertThat(jmxEndpoint.getEndpointId().toLowerCaseString()).isEqualTo("test"); + }); + } + + private Function mockMBeanServer() { + return (ctxRunner) -> ctxRunner.withBean("mbeanServer", MBeanServer.class, () -> this.mBeanServer); + } + + @Endpoint(id = "test") + static class TestEndpoint { + + @ReadOperation + String hello() { + return "hello world"; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc index 9d0df41b08..214e279c6a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc @@ -1426,6 +1426,7 @@ This can be achieved using the configprop:management.endpoints.web.exposure.excl Java Management Extensions (JMX) provide a standard mechanism to monitor and manage applications. By default, this feature is not enabled and can be turned on by setting the configuration property configprop:spring.jmx.enabled[] to `true`. Spring Boot exposes management endpoints as JMX MBeans under the `org.springframework.boot` domain by default. +To Take full control over endpoints registration in the JMX domain, consider registering your own `EndpointObjectNameFactory` implementation.