diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/condition/ConditionsReportEndpoint.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/condition/ConditionsReportEndpoint.java index d9b79e8305..4882adcca5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/condition/ConditionsReportEndpoint.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/condition/ConditionsReportEndpoint.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport; @@ -81,7 +82,7 @@ public class ConditionsReportEndpoint { /** * A description of an application's condition evaluation. */ - public static final class ConditionsDescriptor { + public static final class ConditionsDescriptor implements OperationResponseBody { private final Map contexts; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java new file mode 100644 index 0000000000..4056525887 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2022 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.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Endpoint Jackson support. + * + * @author Phillip Webb + * @since 3.0.0 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(JacksonAutoConfiguration.class) +public class JacksonEndpointAutoConfiguration { + + @Bean + @ConditionalOnProperty(name = "management.endpoints.jackson.isolated-object-mapper", matchIfMissing = true) + @ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class }) + public EndpointObjectMapper endpointObjectMapper() { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build(); + return () -> objectMapper; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/package-info.java new file mode 100644 index 0000000000..3617bb3531 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2022 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. + */ + +/** + * Actuator Jackson auto-configuration. + */ +package org.springframework.boot.actuate.autoconfigure.endpoint.jackson; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index 0ca941a499..faaf723127 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -23,6 +23,10 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.Priority; +import jakarta.ws.rs.Priorities; +import jakarta.ws.rs.ext.ContextResolver; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.model.Resource; @@ -35,7 +39,9 @@ import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnMa import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -53,6 +59,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; import org.springframework.util.StringUtils; @@ -98,6 +105,13 @@ class JerseyWebEndpointManagementContextConfiguration { return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(health, healthEndpointGroups); } + @Bean + @ConditionalOnBean(EndpointObjectMapper.class) + ResourceConfigCustomizer endpointObjectMapperResourceConfigCustomizer(EndpointObjectMapper endpointObjectMapper) { + return (config) -> config.register(new EndpointObjectMapperContextResolver(endpointObjectMapper), + ContextResolver.class); + } + private boolean shouldRegisterLinksMapping(WebEndpointProperties properties, Environment environment, String basePath) { return properties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) @@ -192,4 +206,24 @@ class JerseyWebEndpointManagementContextConfiguration { } + /** + * {@link ContextResolver} used to obtain the {@link ObjectMapper} that should be used + * for {@link OperationResponseBody} instances. + */ + @Priority(Priorities.USER - 100) + private static final class EndpointObjectMapperContextResolver implements ContextResolver { + + private final EndpointObjectMapper endpointObjectMapper; + + private EndpointObjectMapperContextResolver(EndpointObjectMapper endpointObjectMapper) { + this.endpointObjectMapper = endpointObjectMapper; + } + + @Override + public ObjectMapper getContext(Class type) { + return OperationResponseBody.class.isAssignableFrom(type) ? this.endpointObjectMapper.get() : null; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index 6d9d5dc3bc..433303ebcb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -17,9 +17,16 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; @@ -28,7 +35,9 @@ import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfi import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -48,7 +57,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Role; +import org.springframework.core.codec.Encoder; import org.springframework.core.env.Environment; +import org.springframework.http.MediaType; +import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.util.StringUtils; import org.springframework.web.reactive.DispatcherHandler; @@ -114,4 +130,55 @@ public class WebFluxEndpointManagementContextConfiguration { corsProperties.toCorsConfiguration()); } + @Bean + @ConditionalOnBean(EndpointObjectMapper.class) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor serverCodecConfigurerEndpointObjectMapperBeanPostProcessor( + EndpointObjectMapper endpointObjectMapper) { + return new ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor(endpointObjectMapper); + } + + /** + * {@link BeanPostProcessor} to apply {@link EndpointObjectMapper} for + * {@link OperationResponseBody} to {@link Jackson2JsonEncoder} instances. + */ + private static class ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor implements BeanPostProcessor { + + private static final List MEDIA_TYPES = Collections + .unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"))); + + private final EndpointObjectMapper endpointObjectMapper; + + ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor(EndpointObjectMapper endpointObjectMapper) { + this.endpointObjectMapper = endpointObjectMapper; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ServerCodecConfigurer) { + process((ServerCodecConfigurer) bean); + } + return bean; + } + + private void process(ServerCodecConfigurer configurer) { + for (HttpMessageWriter writer : configurer.getWriters()) { + if (writer instanceof EncoderHttpMessageWriter) { + process(((EncoderHttpMessageWriter) writer).getEncoder()); + } + } + } + + private void process(Encoder encoder) { + if (encoder instanceof Jackson2JsonEncoder) { + Jackson2JsonEncoder jackson2JsonEncoder = (Jackson2JsonEncoder) encoder; + jackson2JsonEncoder.registerObjectMappersForType(OperationResponseBody.class, (associations) -> { + ObjectMapper objectMapper = this.endpointObjectMapper.get(); + MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, objectMapper)); + }); + } + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index f60bd8ce8f..07d507aaab 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -17,9 +17,14 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; @@ -28,7 +33,9 @@ import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfi import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -49,9 +56,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Role; import org.springframework.core.env.Environment; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.util.StringUtils; import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * {@link ManagementContextConfiguration @ManagementContextConfiguration} for Spring MVC @@ -116,4 +128,46 @@ public class WebMvcEndpointManagementContextConfiguration { corsProperties.toCorsConfiguration()); } + @Bean + @ConditionalOnBean(EndpointObjectMapper.class) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static EndpointObjectMapperWebMvcConfigurer endpointObjectMapperWebMvcConfigurer( + EndpointObjectMapper endpointObjectMapper) { + return new EndpointObjectMapperWebMvcConfigurer(endpointObjectMapper); + } + + /** + * {@link WebMvcConfigurer} to apply {@link EndpointObjectMapper} for + * {@link OperationResponseBody} to {@link MappingJackson2HttpMessageConverter} + * instances. + */ + static class EndpointObjectMapperWebMvcConfigurer implements WebMvcConfigurer { + + private static final List MEDIA_TYPES = Collections + .unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"))); + + private final EndpointObjectMapper endpointObjectMapper; + + EndpointObjectMapperWebMvcConfigurer(EndpointObjectMapper endpointObjectMapper) { + this.endpointObjectMapper = endpointObjectMapper; + } + + @Override + public void configureMessageConverters(List> converters) { + for (HttpMessageConverter converter : converters) { + if (converter instanceof MappingJackson2HttpMessageConverter) { + configure((MappingJackson2HttpMessageConverter) converter); + } + } + } + + private void configure(MappingJackson2HttpMessageConverter converter) { + converter.registerObjectMappersForType(OperationResponseBody.class, (associations) -> { + ObjectMapper objectMapper = this.endpointObjectMapper.get(); + MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, objectMapper)); + }); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 6cec380cd6..4785a610a6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -60,6 +60,12 @@ "type": "java.lang.Boolean", "description": "Whether to enable or disable all endpoints by default." }, + { + "name": "management.endpoints.jackson.isolated-object-mapper", + "type": "java.lang.Boolean", + "description": "Whether to use an isolated object mapper to serialize endpoint JSON.", + "defaultValue": true + }, { "name": "management.endpoints.jmx.domain", "defaultValue": "org.springframework.boot" diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 99d0c2fb35..a1a38524d9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -17,6 +17,7 @@ org.springframework.boot.actuate.autoconfigure.couchbase.CouchbaseReactiveHealth org.springframework.boot.actuate.autoconfigure.data.elasticsearch.ElasticsearchReactiveHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticsearchRestHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration +org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfigurationTests.java new file mode 100644 index 0000000000..2d76e97fe1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfigurationTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2022 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.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JacksonEndpointAutoConfiguration}. + * + * @author Phillip Webb + */ +class JacksonEndpointAutoConfigurationTests { + + private ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JacksonEndpointAutoConfiguration.class)); + + @Test + void endpointObjectMapperWhenNoProperty() { + this.runner.run((context) -> assertThat(context).hasSingleBean(EndpointObjectMapper.class)); + } + + @Test + void endpointObjectMapperWhenPropertyTrue() { + this.runner.withPropertyValues("management.endpoints.jackson.isolated-object-mapper=true") + .run((context) -> assertThat(context).hasSingleBean(EndpointObjectMapper.class)); + } + + @Test + void endpointObjectMapperWhenPropertyFalse() { + this.runner.withPropertyValues("management.endpoints.jackson.isolated-object-mapper=false") + .run((context) -> assertThat(context).doesNotHaveBean(EndpointObjectMapper.class)); + } + + @Configuration(proxyBeanMethods = false) + static class TestEndpointMapperConfiguration { + + @Bean + TestEndpointObjectMapper testEndpointObjectMapper() { + return new TestEndpointObjectMapper(); + } + + } + + static class TestEndpointObjectMapper implements EndpointObjectMapper { + + @Override + public ObjectMapper get() { + return null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/EndpointObjectMapperConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/EndpointObjectMapperConfiguration.java new file mode 100644 index 0000000000..261412258a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/EndpointObjectMapperConfiguration.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2022 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.integrationtest; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; + +import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +/** + * {@link Configuration @Configuration} that creates an {@link EndpointObjectMapper} that + * reverses all strings. + * + * @author Phillip Webb + */ +@Configuration +class EndpointObjectMapperConfiguration { + + @Bean + EndpointObjectMapper endpointObjectMapper() { + SimpleModule module = new SimpleModule(); + module.addSerializer(String.class, new ReverseStringSerializer()); + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules(module).build(); + return () -> objectMapper; + } + + static class ReverseStringSerializer extends StdScalarSerializer { + + ReverseStringSerializer() { + super(String.class, false); + } + + @Override + public boolean isEmpty(SerializerProvider prov, Object value) { + return ((String) value).isEmpty(); + } + + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException { + serialize(value, gen); + } + + @Override + public final void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider, + TypeSerializer typeSer) throws IOException { + serialize(value, gen); + } + + private void serialize(Object value, JsonGenerator gen) throws IOException { + StringBuilder builder = new StringBuilder((String) value); + gen.writeString(builder.reverse().toString()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java index 1e8de09dc6..29d79fe72d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.integrationtest; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -44,6 +45,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.servlet.DispatcherServlet; +import static org.assertj.core.api.Assertions.assertThat; + /** * Integration tests for the Jersey actuator endpoints. * @@ -87,7 +90,22 @@ class JerseyEndpointIntegrationTests { .responseTimeout(Duration.ofMinutes(5)).build(); client.get().uri("/actuator").exchange().expectStatus().isUnauthorized(); }); + } + @Test + void endpointObjectMapperCanBeApplied() { + WebApplicationContextRunner contextRunner = getContextRunner(new Class[] { EndpointsConfiguration.class, + ResourceConfigConfiguration.class, EndpointObjectMapperConfiguration.class }); + contextRunner.run((context) -> { + int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class) + .getWebServer().getPort(); + WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port) + .responseTimeout(Duration.ofMinutes(5)).build(); + client.get().uri("/actuator/beans").exchange().expectStatus().isOk().expectBody().consumeWith((result) -> { + String json = new String(result.getResponseBody(), StandardCharsets.UTF_8); + assertThat(json).contains("\"scope\":\"notelgnis\""); + }); + }); } protected void testJerseyEndpoints(Class[] userConfigurations) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java index 65bf929442..d192568240 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.integrationtest; +import java.nio.charset.StandardCharsets; + import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration; @@ -36,6 +38,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.web.reactive.server.WebTestClient; +import static org.assertj.core.api.Assertions.assertThat; + /** * Integration tests for the WebFlux actuator endpoints. * @@ -68,6 +72,19 @@ class WebFluxEndpointIntegrationTests { }); } + @Test + void endpointObjectMapperCanBeApplied() { + this.contextRunner.withUserConfiguration(EndpointObjectMapperConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include:*").run((context) -> { + WebTestClient client = createWebTestClient(context); + client.get().uri("/actuator/beans").exchange().expectStatus().isOk().expectBody() + .consumeWith((result) -> { + String json = new String(result.getResponseBody(), StandardCharsets.UTF_8); + assertThat(json).contains("\"scope\":\"notelgnis\""); + }); + }); + } + private WebTestClient createWebTestClient(ApplicationContext context) { return WebTestClient.bindToApplicationContext(context).configureClient().baseUrl("https://spring.example.org") .build(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java index ca6eea6b82..0f13ee4301 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java @@ -52,6 +52,7 @@ import org.springframework.mock.web.MockServletContext; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.test.context.TestSecurityContextHolder; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; @@ -140,6 +141,16 @@ class WebMvcEndpointIntegrationTests { mockMvc.perform(get("/actuator").accept("*/*")).andExpect(status().isNotFound()); } + @Test + void endpointObjectMapperCanBeApplied() throws Exception { + this.context = new AnnotationConfigServletWebApplicationContext(); + this.context.register(EndpointObjectMapperConfiguration.class, DefaultConfiguration.class); + TestPropertyValues.of("management.endpoints.web.exposure.include=*").applyTo(this.context); + MockMvc mockMvc = doCreateMockMvc(); + MvcResult result = mockMvc.perform(get("/actuator/beans")).andExpect(status().isOk()).andReturn(); + assertThat(result.getResponse().getContentAsString()).contains("\"scope\":\"notelgnis\""); + } + private MockMvc createSecureMockMvc() { return doCreateMockMvc(springSecurity()); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEventsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEventsEndpoint.java index d656f2dbd0..57a2f1f0cf 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEventsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEventsEndpoint.java @@ -20,6 +20,7 @@ import java.time.Instant; import java.time.OffsetDateTime; import java.util.List; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.lang.Nullable; @@ -55,7 +56,7 @@ public class AuditEventsEndpoint { /** * Description of an application's {@link AuditEvent audit events}. */ - public static final class AuditEventsDescriptor { + public static final class AuditEventsDescriptor implements OperationResponseBody { private final List events; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/beans/BeansEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/beans/BeansEndpoint.java index f782902279..2fb51d8f29 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/beans/BeansEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/beans/BeansEndpoint.java @@ -22,6 +22,7 @@ import java.util.Map; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.context.ApplicationContext; @@ -73,7 +74,7 @@ public class BeansEndpoint { /** * Description of an application's beans. */ - public static final class BeansDescriptor { + public static final class BeansDescriptor implements OperationResponseBody { private final Map contexts; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpoint.java index eb2217f0a8..5491a81026 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpoint.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.Objects; import java.util.function.Predicate; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; @@ -148,7 +149,7 @@ public class CachesEndpoint { /** * Description of the caches. */ - public static final class CachesDescriptor { + public static final class CachesDescriptor implements OperationResponseBody { private final Map cacheManagers; @@ -182,7 +183,7 @@ public class CachesEndpoint { /** * Description of a {@link Cache}. */ - public static class CacheDescriptor { + public static class CacheDescriptor implements OperationResponseBody { private final String target; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/ShutdownEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/ShutdownEndpoint.java index 488069cfe1..f081b29438 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/ShutdownEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/ShutdownEndpoint.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.context; import org.springframework.beans.BeansException; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.context.ApplicationContext; @@ -71,7 +72,7 @@ public class ShutdownEndpoint implements ApplicationContextAware { /** * Description of the shutdown. */ - public static class ShutdownDescriptor { + public static class ShutdownDescriptor implements OperationResponseBody { private static final ShutdownDescriptor DEFAULT = new ShutdownDescriptor("Shutting down, bye..."); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java index 0b6250fd44..cbbe397085 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java @@ -54,6 +54,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.SanitizableData; import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.boot.actuate.endpoint.SanitizingFunction; @@ -564,7 +565,7 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext * Description of an application's * {@link ConfigurationProperties @ConfigurationProperties} beans. */ - public static final class ConfigurationPropertiesDescriptor { + public static final class ConfigurationPropertiesDescriptor implements OperationResponseBody { private final Map contexts; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Operation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Operation.java index d68856a078..d21f2fbfc6 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Operation.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Operation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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. @@ -32,7 +32,9 @@ public interface Operation { OperationType getType(); /** - * Invoke the underlying operation using the given {@code context}. + * Invoke the underlying operation using the given {@code context}. Results intended + * to be returned in the body of the response should additionally implement + * {@link OperationResponseBody}. * @param context the context in to use when invoking the operation * @return the result of the operation, may be {@code null} */ diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationResponseBody.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationResponseBody.java new file mode 100644 index 0000000000..6901e54355 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationResponseBody.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2022 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; + +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Tagging interface used to indicate that an operation result is intended to be returned + * in the body of the response. Primarily intended to support JSON serialzation using an + * endpoint specific {@link ObjectMapper}. + * + * @author Phillip Webb + * @since 3.0.0 + */ +public interface OperationResponseBody { + + /** + * Return a {@link OperationResponseBody} {@link Map} instance containing entires from + * the given {@code map}. + * @param the key type + * @param the value type + * @param map the source map or {@code null} + * @return a {@link OperationResponseBody} version of the map or {@code null} + */ + static Map of(Map map) { + return (map != null) ? new OperationResponseBodyMap<>(map) : null; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationResponseBodyMap.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationResponseBodyMap.java new file mode 100644 index 0000000000..c1863686cb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationResponseBodyMap.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2022 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; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * {@link LinkedHashMap} to support {@link OperationResponseBody#of(java.util.Map)}. + * + * @param the key type + * @param the value type + * @author Phillip Webb + */ +class OperationResponseBodyMap extends LinkedHashMap implements OperationResponseBody { + + OperationResponseBodyMap(Map map) { + super(map); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jackson/EndpointObjectMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jackson/EndpointObjectMapper.java new file mode 100644 index 0000000000..930011a25c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jackson/EndpointObjectMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2022 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.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.boot.actuate.endpoint.OperationResponseBody; + +/** + * Interface used to supply the {@link ObjectMapper} that should be used when serializing + * {@link OperationResponseBody} endpoint results. + * + * @author Phillip Webb + * @since 3.0.0 + * @see OperationResponseBody + */ +public interface EndpointObjectMapper { + + /** + * Return the {@link ObjectMapper} that should be used to serialize + * {@link OperationResponseBody} endpoint results. + * @return the object mapper + */ + ObjectMapper get(); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jackson/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jackson/package-info.java new file mode 100644 index 0000000000..a2e42218d2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jackson/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2022 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. + */ + +/** + * Jackson support classes for actuator endpoints. + */ +package org.springframework.boot.actuate.endpoint.jackson; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java index 0b80224ea4..f9b7d7f008 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java @@ -43,6 +43,7 @@ import reactor.core.publisher.Mono; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.OperationArgumentResolver; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; @@ -323,7 +324,8 @@ public class JerseyEndpointResourceFactory { public Response apply(ContainerRequestContext request) { Map links = this.linksResolver .resolveLinks(request.getUriInfo().getAbsolutePath().toString()); - return Response.ok(Collections.singletonMap("_links", links)).build(); + Map> entity = OperationResponseBody.of(Collections.singletonMap("_links", links)); + return Response.ok(entity).build(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java index aafa6c707b..8acbbcb85e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java @@ -26,6 +26,7 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.annotation.Reflective; import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -87,8 +88,8 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle public Map> links(ServerWebExchange exchange) { String requestUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).replaceQuery(null) .toUriString(); - return Collections.singletonMap("_links", - WebFluxEndpointHandlerMapping.this.linksResolver.resolveLinks(requestUri)); + Map links = WebFluxEndpointHandlerMapping.this.linksResolver.resolveLinks(requestUri); + return OperationResponseBody.of(Collections.singletonMap("_links", links)); } @Override diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java index 180a378bbe..961abc93c3 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java @@ -28,6 +28,7 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.annotation.Reflective; import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -84,8 +85,9 @@ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerM @ResponseBody @Reflective public Map> links(HttpServletRequest request, HttpServletResponse response) { - return Collections.singletonMap("_links", - WebMvcEndpointHandlerMapping.this.linksResolver.resolveLinks(request.getRequestURL().toString())); + Map links = WebMvcEndpointHandlerMapping.this.linksResolver + .resolveLinks(request.getRequestURL().toString()); + return OperationResponseBody.of(Collections.singletonMap("_links", links)); } @Override diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java index c148c0f134..0d01c3aac4 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java @@ -27,6 +27,7 @@ import java.util.stream.Stream; import com.fasterxml.jackson.annotation.JsonInclude; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.SanitizableData; import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.boot.actuate.endpoint.SanitizingFunction; @@ -203,7 +204,7 @@ public class EnvironmentEndpoint { /** * Description of an {@link Environment}. */ - public static final class EnvironmentDescriptor { + public static final class EnvironmentDescriptor implements OperationResponseBody { private final List activeProfiles; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/flyway/FlywayEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/flyway/FlywayEndpoint.java index 03da1ee553..00f1aa3aab 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/flyway/FlywayEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/flyway/FlywayEndpoint.java @@ -28,6 +28,7 @@ import org.flywaydb.core.Flyway; import org.flywaydb.core.api.MigrationInfo; import org.flywaydb.core.api.MigrationState; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.context.ApplicationContext; @@ -69,7 +70,7 @@ public class FlywayEndpoint { /** * Description of an application's {@link Flyway} beans. */ - public static final class FlywayBeansDescriptor { + public static final class FlywayBeansDescriptor implements OperationResponseBody { private final Map contexts; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthComponent.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthComponent.java index 2b37b0a176..aa80a8fb1d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthComponent.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -18,6 +18,8 @@ package org.springframework.boot.actuate.health; import com.fasterxml.jackson.annotation.JsonUnwrapped; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; + /** * A component that contributes data to results returned from the {@link HealthEndpoint}. * @@ -26,7 +28,7 @@ import com.fasterxml.jackson.annotation.JsonUnwrapped; * @see Health * @see CompositeHealth */ -public abstract class HealthComponent { +public abstract class HealthComponent implements OperationResponseBody { HealthComponent() { } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/InfoEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/InfoEndpoint.java index 57b1e6f655..2c48fc18d2 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/InfoEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/InfoEndpoint.java @@ -19,6 +19,7 @@ package org.springframework.boot.actuate.info; import java.util.List; import java.util.Map; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.util.Assert; @@ -51,7 +52,7 @@ public class InfoEndpoint { for (InfoContributor contributor : this.infoContributors) { contributor.contribute(builder); } - return builder.build().getDetails(); + return OperationResponseBody.of(builder.build().getDetails()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpoint.java index a741c0b27c..71fe3de110 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpoint.java @@ -22,6 +22,7 @@ import java.util.Map; import org.springframework.aot.hint.BindingReflectionHintsRegistrar; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; @@ -99,7 +100,7 @@ public class IntegrationGraphEndpoint { /** * Description of a {@link Graph}. */ - public static class GraphDescriptor { + public static class GraphDescriptor implements OperationResponseBody { private final Map contentDescriptor; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpoint.java index 73b3c9867a..ba709128ac 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpoint.java @@ -32,6 +32,7 @@ import liquibase.database.DatabaseFactory; import liquibase.database.jvm.JdbcConnection; import liquibase.integration.spring.SpringLiquibase; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.context.ApplicationContext; @@ -106,7 +107,7 @@ public class LiquibaseEndpoint { /** * Description of an application's {@link SpringLiquibase} beans. */ - public static final class LiquibaseBeansDescriptor { + public static final class LiquibaseBeansDescriptor implements OperationResponseBody { private final Map contexts; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java index d4c1f1aa1c..09a94dc655 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java @@ -25,6 +25,7 @@ import java.util.Set; import java.util.TreeSet; import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; @@ -121,7 +122,7 @@ public class LoggersEndpoint { /** * Description of loggers. */ - public static class LoggersDescriptor { + public static class LoggersDescriptor implements OperationResponseBody { /** * Empty description. @@ -158,7 +159,7 @@ public class LoggersEndpoint { /** * Description of levels configured for a given logger. */ - public static class LoggerLevelsDescriptor { + public static class LoggerLevelsDescriptor implements OperationResponseBody { private String configuredLevel; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/ThreadDumpEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/ThreadDumpEndpoint.java index eaa5c238b4..03dc19f7e5 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/ThreadDumpEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/ThreadDumpEndpoint.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.List; import java.util.function.Function; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; @@ -54,7 +55,7 @@ public class ThreadDumpEndpoint { /** * Description of a thread dump. */ - public static final class ThreadDumpDescriptor { + public static final class ThreadDumpDescriptor implements OperationResponseBody { private final List threads; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java index d7834552e6..1ebabacf8f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java @@ -34,6 +34,7 @@ import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.composite.CompositeMeterRegistry; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; @@ -159,7 +160,7 @@ public class MetricsEndpoint { /** * Description of metric names. */ - public static final class MetricNamesDescriptor { + public static final class MetricNamesDescriptor implements OperationResponseBody { private final Set names; @@ -176,7 +177,7 @@ public class MetricsEndpoint { /** * Description of a metric. */ - public static final class MetricDescriptor { + public static final class MetricDescriptor implements OperationResponseBody { private final String name; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpoint.java index e2df8766e5..de40cb092f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpoint.java @@ -47,6 +47,7 @@ import org.quartz.Trigger.TriggerState; import org.quartz.TriggerKey; import org.quartz.impl.matchers.GroupMatcher; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.SanitizableData; import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.boot.actuate.endpoint.SanitizingFunction; @@ -236,8 +237,9 @@ public class QuartzEndpoint { return null; } TriggerState triggerState = this.scheduler.getTriggerState(triggerKey); - return TriggerDescriptor.of(trigger).buildDetails(triggerState, - sanitizeJobDataMap(trigger.getJobDataMap(), showUnsanitized)); + TriggerDescriptor triggerDescriptor = TriggerDescriptor.of(trigger); + Map jobDataMap = sanitizeJobDataMap(trigger.getJobDataMap(), showUnsanitized); + return OperationResponseBody.of(triggerDescriptor.buildDetails(triggerState, jobDataMap)); } private static Duration getIntervalDuration(long amount, IntervalUnit unit) { @@ -279,7 +281,7 @@ public class QuartzEndpoint { /** * Description of available job and trigger group names. */ - public static final class QuartzDescriptor { + public static final class QuartzDescriptor implements OperationResponseBody { private final GroupNamesDescriptor jobs; @@ -320,7 +322,7 @@ public class QuartzEndpoint { /** * Description of each group identified by name. */ - public static class QuartzGroupsDescriptor { + public static class QuartzGroupsDescriptor implements OperationResponseBody { private final Map groups; @@ -337,7 +339,7 @@ public class QuartzEndpoint { /** * Description of the {@link JobDetail jobs} in a given group. */ - public static final class QuartzJobGroupSummaryDescriptor { + public static final class QuartzJobGroupSummaryDescriptor implements OperationResponseBody { private final String group; @@ -382,7 +384,7 @@ public class QuartzEndpoint { /** * Description of a {@link Job Quartz Job}. */ - public static final class QuartzJobDetailsDescriptor { + public static final class QuartzJobDetailsDescriptor implements OperationResponseBody { private final String group; @@ -449,7 +451,7 @@ public class QuartzEndpoint { /** * Description of the {@link Trigger triggers} in a given group. */ - public static final class QuartzTriggerGroupSummaryDescriptor { + public static final class QuartzTriggerGroupSummaryDescriptor implements OperationResponseBody { private final String group; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpoint.java index 73dc40e7b8..05b56708f3 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpoint.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import org.springframework.aot.hint.BindingReflectionHintsRegistrar; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint.ScheduledTasksEndpointRuntimeHints; @@ -75,7 +76,7 @@ public class ScheduledTasksEndpoint { /** * Description of an application's scheduled {@link Task Tasks}. */ - public static final class ScheduledTasksDescriptor { + public static final class ScheduledTasksDescriptor implements OperationResponseBody { private final List cron; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java index 2727299452..b333d8e23c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; @@ -70,7 +71,7 @@ public class SessionsEndpoint { /** * Description of user's {@link Session sessions}. */ - public static final class SessionsDescriptor { + public static final class SessionsDescriptor implements OperationResponseBody { private final List sessions; @@ -87,7 +88,7 @@ public class SessionsEndpoint { /** * Description of user's {@link Session session}. */ - public static final class SessionDescriptor { + public static final class SessionDescriptor implements OperationResponseBody { private final String id; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/StartupEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/StartupEndpoint.java index 4d5f50f9da..a713fa7b5d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/StartupEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/StartupEndpoint.java @@ -21,6 +21,7 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.boot.SpringBootVersion; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; @@ -68,7 +69,7 @@ public class StartupEndpoint { /** * Description of an application startup. */ - public static final class StartupDescriptor { + public static final class StartupDescriptor implements OperationResponseBody { private final String springBootVersion; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchangesEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchangesEndpoint.java index 58041ae402..a683709327 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchangesEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchangesEndpoint.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.web.exchanges; import java.util.List; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.util.Assert; @@ -51,7 +52,7 @@ public class HttpExchangesEndpoint { /** * Description of an application's {@link HttpExchange} entries. */ - public static final class HttpExchangesDescriptor { + public static final class HttpExchangesDescriptor implements OperationResponseBody { private final List exchanges; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/MappingsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/MappingsEndpoint.java index de23192682..e9b7866286 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/MappingsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/MappingsEndpoint.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.context.ApplicationContext; @@ -64,7 +65,7 @@ public class MappingsEndpoint { /** * Description of an application's request mappings. */ - public static final class ApplicationMappingsDescriptor { + public static final class ApplicationMappingsDescriptor implements OperationResponseBody { private final Map contextMappings; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/OperationResponseBodyTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/OperationResponseBodyTests.java new file mode 100644 index 0000000000..7c62d7a8ec --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/OperationResponseBodyTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2022 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; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link OperationResponseBody}. + * + * @author Phillip Webb + */ +class OperationResponseBodyTests { + + @Test + void ofMapReturnsOperationResponseBody() { + LinkedHashMap map = new LinkedHashMap<>(); + map.put("one", "1"); + map.put("two", "2"); + Map mapDescriptor = OperationResponseBody.of(map); + assertThat(mapDescriptor).containsExactly(entry("one", "1"), entry("two", "2")); + assertThat(mapDescriptor).isInstanceOf(OperationResponseBody.class); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointTests.java index 2a32034e78..c7f4b7572a 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointTests.java @@ -68,19 +68,19 @@ class IntegrationGraphEndpointTests { @Test void readOperationShouldReturnGraph() { - Graph mockedGraph = mock(Graph.class); + Graph graph = mock(Graph.class); Map contentDescriptor = new LinkedHashMap<>(); Collection nodes = new ArrayList<>(); Collection links = new ArrayList<>(); - given(mockedGraph.getContentDescriptor()).willReturn(contentDescriptor); - given(mockedGraph.getNodes()).willReturn(nodes); - given(mockedGraph.getLinks()).willReturn(links); - given(this.server.getGraph()).willReturn(mockedGraph); - GraphDescriptor graph = this.endpoint.graph(); + given(graph.getContentDescriptor()).willReturn(contentDescriptor); + given(graph.getNodes()).willReturn(nodes); + given(graph.getLinks()).willReturn(links); + given(this.server.getGraph()).willReturn(graph); + GraphDescriptor descriptor = this.endpoint.graph(); then(this.server).should().getGraph(); - assertThat(graph.getContentDescriptor()).isSameAs(contentDescriptor); - assertThat(graph.getNodes()).isSameAs(nodes); - assertThat(graph.getLinks()).isSameAs(links); + assertThat(descriptor.getContentDescriptor()).isSameAs(contentDescriptor); + assertThat(descriptor.getNodes()).isSameAs(nodes); + assertThat(descriptor.getLinks()).isSameAs(links); } @Test diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/java/smoketest/actuator/SampleActuatorApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/java/smoketest/actuator/SampleActuatorApplication.java index d98a9092b9..f7264d52bf 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/java/smoketest/actuator/SampleActuatorApplication.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/java/smoketest/actuator/SampleActuatorApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -25,6 +25,7 @@ import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthContributor; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.context.annotation.Bean; @@ -32,10 +33,6 @@ import org.springframework.context.annotation.Bean; @ConfigurationPropertiesScan public class SampleActuatorApplication { - public static void main(String[] args) { - SpringApplication.run(SampleActuatorApplication.class, args); - } - @Bean public HealthIndicator helloHealthIndicator() { return createHealthIndicator("world"); @@ -61,4 +58,10 @@ public class SampleActuatorApplication { return () -> Health.up().withDetail("hello", value).build(); } + public static void main(String[] args) { + SpringApplication application = new SpringApplication(SampleActuatorApplication.class); + application.setApplicationStartup(new BufferingApplicationStartup(1024)); + application.run(args); + } + } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties index 2a05e8284e..81cc777bfc 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties @@ -28,3 +28,5 @@ management.endpoint.health.group.comp.show-details=always management.endpoints.migrate-legacy-ids=true +management.endpoints.jackson.isolated-object-mapper=true +spring.jackson.visibility.field=any diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ApplicationStartupSpringBootContextLoader.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ApplicationStartupSpringBootContextLoader.java new file mode 100644 index 0000000000..584274fddb --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ApplicationStartupSpringBootContextLoader.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2022 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 smoketest.actuator; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; +import org.springframework.boot.test.context.SpringBootContextLoader; + +class ApplicationStartupSpringBootContextLoader extends SpringBootContextLoader { + + @Override + protected SpringApplication getSpringApplication() { + SpringApplication application = new SpringApplication(); + application.setApplicationStartup(new BufferingApplicationStartup(1024)); + return application; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedObjectMapperFalseTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedObjectMapperFalseTests.java new file mode 100644 index 0000000000..4a77556a88 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedObjectMapperFalseTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2022 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 smoketest.actuator; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for WebMVC actuator when using an isolated {@link ObjectMapper}. + * + * @author Phillip Webb + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "management.endpoints.jackson.isolated-object-mapper=false") +@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) +class SampleActuatorApplicationIsolatedObjectMapperFalseTests { + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void resourceShouldBeAvailableOnMainPort() { + ResponseEntity entity = this.testRestTemplate.withBasicAuth("user", "password") + .getForEntity("/actuator/startup", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedObjectMapperTrueTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedObjectMapperTrueTests.java new file mode 100644 index 0000000000..120da39375 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationIsolatedObjectMapperTrueTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2022 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 smoketest.actuator; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for WebMVC actuator when using an isolated {@link ObjectMapper}. + * + * @author Phillip Webb + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "management.endpoints.jackson.isolated-object-mapper=true") +@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) +class SampleActuatorApplicationIsolatedObjectMapperTrueTests { + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void resourceShouldBeAvailableOnMainPort() { + ResponseEntity entity = this.testRestTemplate.withBasicAuth("user", "password") + .getForEntity("/actuator/startup", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"timeline\":"); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/SampleJerseyApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/SampleJerseyApplication.java index 0027e21df5..d44e50c5d4 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/SampleJerseyApplication.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/SampleJerseyApplication.java @@ -18,13 +18,15 @@ package smoketest.jersey; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; @SpringBootApplication public class SampleJerseyApplication extends SpringBootServletInitializer { public static void main(String[] args) { - new SampleJerseyApplication().configure(new SpringApplicationBuilder(SampleJerseyApplication.class)).run(args); + new SampleJerseyApplication().configure(new SpringApplicationBuilder(SampleJerseyApplication.class) + .applicationStartup(new BufferingApplicationStartup(2048))).run(args); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/resources/application.properties new file mode 100644 index 0000000000..641c39e657 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/resources/application.properties @@ -0,0 +1,3 @@ +management.endpoints.web.exposure.include=* +management.endpoints.jackson.isolated-object-mapper=true +spring.jackson.visibility.field=any diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/ApplicationStartupSpringBootContextLoader.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/ApplicationStartupSpringBootContextLoader.java new file mode 100644 index 0000000000..4d16b9e19d --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/ApplicationStartupSpringBootContextLoader.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2022 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 smoketest.jersey; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; +import org.springframework.boot.test.context.SpringBootContextLoader; + +class ApplicationStartupSpringBootContextLoader extends SpringBootContextLoader { + + @Override + protected SpringApplication getSpringApplication() { + SpringApplication application = new SpringApplication(); + application.setApplicationStartup(new BufferingApplicationStartup(1024)); + return application; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperFalseTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperFalseTests.java new file mode 100644 index 0000000000..e359fd94d2 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperFalseTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2022 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 smoketest.jersey; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for Jersey actuator when not using an isolated {@link ObjectMapper}. + * + * @author Phillip Webb + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "management.endpoints.jackson.isolated-object-mapper=false") +@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) +public class JerseyActuatorIsolatedObjectMapperFalseTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void resourceShouldBeAvailableOnMainPort() { + ResponseEntity entity = this.testRestTemplate + .getForEntity("http://localhost:" + this.port + "/actuator/startup", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(entity.getBody()) + .contains("Java 8 date/time type `java.time.Clock$SystemClock` not supported by default"); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperTrueTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperTrueTests.java new file mode 100644 index 0000000000..57a084743e --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperTrueTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2022 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 smoketest.jersey; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for Jersey actuator when using an isolated {@link ObjectMapper}. + * + * @author Phillip Webb + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "management.endpoints.jackson.isolated-object-mapper=true") +@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) +public class JerseyActuatorIsolatedObjectMapperTrueTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void resourceShouldBeAvailableOnMainPort() { + ResponseEntity entity = this.testRestTemplate + .getForEntity("http://localhost:" + this.port + "/actuator/startup", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"timeline\":"); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/main/java/smoketest/webflux/SampleWebFluxApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/main/java/smoketest/webflux/SampleWebFluxApplication.java index f31e090c01..d8e0e04cf3 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/main/java/smoketest/webflux/SampleWebFluxApplication.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/main/java/smoketest/webflux/SampleWebFluxApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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. @@ -18,6 +18,7 @@ package smoketest.webflux; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; import org.springframework.context.annotation.Bean; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; @@ -29,7 +30,9 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r public class SampleWebFluxApplication { public static void main(String[] args) { - SpringApplication.run(SampleWebFluxApplication.class, args); + SpringApplication application = new SpringApplication(SampleWebFluxApplication.class); + application.setApplicationStartup(new BufferingApplicationStartup(1024)); + application.run(args); } @Bean diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/main/resources/application.properties new file mode 100644 index 0000000000..641c39e657 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/main/resources/application.properties @@ -0,0 +1,3 @@ +management.endpoints.web.exposure.include=* +management.endpoints.jackson.isolated-object-mapper=true +spring.jackson.visibility.field=any diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/ApplicationStartupSpringBootContextLoader.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/ApplicationStartupSpringBootContextLoader.java new file mode 100644 index 0000000000..d6b13bbf8c --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/ApplicationStartupSpringBootContextLoader.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2022 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 smoketest.webflux; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; +import org.springframework.boot.test.context.SpringBootContextLoader; + +class ApplicationStartupSpringBootContextLoader extends SpringBootContextLoader { + + @Override + protected SpringApplication getSpringApplication() { + SpringApplication application = new SpringApplication(); + application.setApplicationStartup(new BufferingApplicationStartup(1024)); + return application; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/WebFluxDifferentPortSampleActuatorApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorDifferentPortTests.java similarity index 96% rename from spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/WebFluxDifferentPortSampleActuatorApplicationTests.java rename to spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorDifferentPortTests.java index d22383653c..74b09a6dda 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/WebFluxDifferentPortSampleActuatorApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorDifferentPortTests.java @@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat; */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { "management.server.port=0", "management.endpoints.web.base-path=/" }) -class WebFluxDifferentPortSampleActuatorApplicationTests { +class SampleWebFluxApplicationActuatorDifferentPortTests { @LocalManagementPort private int managementPort; diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorIsolatedObjectMapperFalseTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorIsolatedObjectMapperFalseTests.java new file mode 100644 index 0000000000..5db1210165 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorIsolatedObjectMapperFalseTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2022 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 smoketest.webflux; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Integration test for WebFlux actuator when using an isolated {@link ObjectMapper}. + * + * @author Phillip Webb + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "management.endpoints.jackson.isolated-object-mapper=false") +@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) +class SampleWebFluxApplicationActuatorIsolatedObjectMapperFalseTests { + + @Autowired + private WebTestClient webClient; + + @Test + void linksEndpointShouldBeAvailable() { + this.webClient.get().uri("/actuator/startup").accept(MediaType.APPLICATION_JSON).exchange().expectStatus() + .is5xxServerError(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorIsolatedObjectMapperTrueTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorIsolatedObjectMapperTrueTests.java new file mode 100644 index 0000000000..567d5703ec --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/test/java/smoketest/webflux/SampleWebFluxApplicationActuatorIsolatedObjectMapperTrueTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2022 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 smoketest.webflux; + +import java.nio.charset.StandardCharsets; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for WebFlux actuator when not using an isolated {@link ObjectMapper}. + * + * @author Phillip Webb + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "management.endpoints.jackson.isolated-object-mapper=true") +@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) +class SampleWebFluxApplicationActuatorIsolatedObjectMapperTrueTests { + + @Autowired + private WebTestClient webClient; + + @Test + void linksEndpointShouldBeAvailable() { + this.webClient.get().uri("/actuator/startup").accept(MediaType.APPLICATION_JSON).exchange().expectStatus() + .isOk().expectBody().consumeWith(this::assertExpectedJson); + } + + private void assertExpectedJson(EntityExchangeResult result) { + String body = new String(result.getResponseBody(), StandardCharsets.UTF_8); + assertThat(body).contains("\"timeline\":"); + } + +}