diff --git a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java index 5047886c583..b69ff5848c1 100644 --- a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java +++ b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.jmx; import javax.management.MBeanServer; +import com.fasterxml.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.ObjectProvider; @@ -44,6 +45,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; @@ -133,4 +135,24 @@ public final class JmxEndpointAutoConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ObjectMapper.class) + @ConditionalOnMissingClass("tools.jackson.databind.json.JsonMapper") + @Deprecated(since = "4.2.0", forRemoval = true) + @SuppressWarnings("removal") + static class JmxJackson2EndpointConfiguration { + + @Bean + @ConditionalOnSingleCandidate(MBeanServer.class) + JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, + EndpointObjectNameFactory endpointObjectNameFactory, ObjectProvider objectMapper, + JmxEndpointsSupplier jmxEndpointsSupplier) { + JmxOperationResponseMapper responseMapper = new org.springframework.boot.actuate.endpoint.jmx.Jackson2JmxOperationResponseMapper( + objectMapper.getIfAvailable()); + return new JmxEndpointExporter(mBeanServer, endpointObjectNameFactory, responseMapper, + jmxEndpointsSupplier.getEndpoints()); + } + + } + } diff --git a/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/Jackson2JmxOperationResponseMapper.java b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/Jackson2JmxOperationResponseMapper.java new file mode 100644 index 00000000000..2a2f1d03bdd --- /dev/null +++ b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/Jackson2JmxOperationResponseMapper.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-present 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.endpoint.jmx; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.jspecify.annotations.Nullable; + +import org.springframework.lang.Contract; + +/** + * {@link JmxOperationResponseMapper} that delegates to a Jackson 2 {@link ObjectMapper} + * to return a JSON response. + * + * @author Stephane Nicoll + * @since 4.0.0 + * @deprecated since 4.0.0 for removal in 4.2.0 in favor of + * {@link JacksonJmxOperationResponseMapper}. + */ +@Deprecated(since = "4.0.0", forRemoval = true) +public class Jackson2JmxOperationResponseMapper implements JmxOperationResponseMapper { + + private final ObjectMapper objectMapper; + + private final JavaType listType; + + private final JavaType mapType; + + public Jackson2JmxOperationResponseMapper(@Nullable ObjectMapper objectMapper) { + this.objectMapper = (objectMapper != null) ? objectMapper : new ObjectMapper(); + this.listType = this.objectMapper.getTypeFactory().constructParametricType(List.class, Object.class); + this.mapType = this.objectMapper.getTypeFactory() + .constructParametricType(Map.class, String.class, Object.class); + } + + @Override + public Class mapResponseType(Class responseType) { + if (CharSequence.class.isAssignableFrom(responseType)) { + return String.class; + } + if (responseType.isArray() || Collection.class.isAssignableFrom(responseType)) { + return List.class; + } + return Map.class; + } + + @Override + @Contract("!null -> !null") + public @Nullable Object mapResponse(@Nullable Object response) { + if (response == null) { + return null; + } + if (response instanceof CharSequence) { + return response.toString(); + } + if (response.getClass().isArray() || response instanceof Collection) { + return this.objectMapper.convertValue(response, this.listType); + } + return this.objectMapper.convertValue(response, this.mapType); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-jackson2-only/src/main/resources/application.properties b/smoke-test/spring-boot-smoke-test-jackson2-only/src/main/resources/application.properties index 821da092741..986f39fad8a 100644 --- a/smoke-test/spring-boot-smoke-test-jackson2-only/src/main/resources/application.properties +++ b/smoke-test/spring-boot-smoke-test-jackson2-only/src/main/resources/application.properties @@ -1 +1,3 @@ +management.endpoints.jmx.exposure.include=* management.endpoints.web.exposure.include=* + diff --git a/smoke-test/spring-boot-smoke-test-jackson2-only/src/test/java/smoketest/jackson2/only/SampleJackson2OnlyApplicationTests.java b/smoke-test/spring-boot-smoke-test-jackson2-only/src/test/java/smoketest/jackson2/only/SampleJackson2OnlyApplicationTests.java index fce0b81cb12..2461c9ac39e 100644 --- a/smoke-test/spring-boot-smoke-test-jackson2-only/src/test/java/smoketest/jackson2/only/SampleJackson2OnlyApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-jackson2-only/src/test/java/smoketest/jackson2/only/SampleJackson2OnlyApplicationTests.java @@ -16,8 +16,12 @@ package smoketest.jackson2.only; +import java.lang.management.ManagementFactory; import java.util.Map; +import javax.management.MBeanServer; +import javax.management.ObjectName; + import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -33,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Andy Wilkinson */ -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.jmx.enabled=true") @AutoConfigureRestTestClient class SampleJackson2OnlyApplicationTests { @@ -64,4 +68,14 @@ class SampleJackson2OnlyApplicationTests { .value((body) -> assertThat(body).containsOnlyKeys("_links")); } + @Test + @SuppressWarnings("unchecked") + void jmxEndpointsShouldWork() throws Exception { + MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); + Map result = (Map) mbeanServer.invoke( + ObjectName.getInstance("org.springframework.boot:type=Endpoint,name=Configprops"), + "configurationProperties", new Object[0], null); + assertThat(result).containsOnlyKeys("contexts"); + } + }